* QtHttpServer
* QtLocation
* QtAsyncio
+* QtWebView
### Documentation and Bugs
_pyside_tools = available_pyside_tools(qt_tools_path=qt_install_path)
# replacing pyside6-android_deploy by pyside6-android-deploy for consistency
- # Also, the tool should not exist in any other platform than Linux
+ # Also, the tool should not exist in any other platform than Linux and macOS
_console_scripts = []
- if ("android_deploy" in _pyside_tools) and sys.platform.startswith("linux"):
+ if ("android_deploy" in _pyside_tools) and sys.platform in ["linux", "darwin"]:
_console_scripts = [(f"{PYSIDE}-android-deploy ="
" PySide6.scripts.pyside_tool:android_deploy")]
_pyside_tools.remove("android_deploy")
import platform
import re
import sys
+import subprocess
import sysconfig
import time
from packaging.version import parse as parse_version
qt_src_dir = maybe_qt_src_dir
+def get_soname(clang_lib_path: Path) -> str:
+ """Getting SONAME from a shared library using readelf. Works only on Linux.
+ """
+ clang_lib_path = Path(clang_lib_path)
+ try:
+ result = subprocess.run(['readelf', '-d', str(clang_lib_path)],
+ capture_output=True, text=True, check=True)
+ for line in result.stdout.split('\n'):
+ if 'SONAME' in line:
+ soname = line.split('[')[1].split(']')[0]
+ return soname
+ except subprocess.CalledProcessError as e:
+ print(f"Failed to get SONAME: {e}")
+ return None
+
+
class PysideInstall(_install, CommandMixin):
user_options = _install.user_options + CommandMixin.mixin_user_options
log.info("-" * 3)
if sys.platform == 'win32':
log.info(f"OpenSSL dll directory: {OPTION['OPENSSL']}")
- if sys.platform == 'darwin':
+ # for cross-compilation it is possible to use a macOS host, but
+ # pyside_macos_deployment_target is not relevant for the target.
+ # The only exception here is when we are trying to cross-compile from intel mac to m1 mac.
+ # This case is not supported yet.
+ if sys.platform == 'darwin' and not self.is_cross_compile:
pyside_macos_deployment_target = (macos_pyside_min_deployment_target())
log.info(f"MACOSX_DEPLOYMENT_TARGET set to: {pyside_macos_deployment_target}")
log.info("=" * 30)
cmake_cmd.append(f"-DCMAKE_UNITY_BUILD_BATCH_SIZE={batch_size}")
log.info("Using UNITY build")
+ if OPTION['SHIBOKEN_FORCE_PROCESS_SYSTEM_HEADERS']:
+ cmake_cmd.append("-DPYSIDE_TREAT_QT_INCLUDE_DIRS_AS_NON_SYSTEM=ON")
+ log.info("Shiboken will now process system Qt headers")
+
+ if OPTION['SHIBOKEN_EXTRA_INCLUDE_PATHS']:
+ extra_include_paths = ';'.join(OPTION['SHIBOKEN_EXTRA_INCLUDE_PATHS'].split(','))
+ cmake_cmd.append(f"-DSHIBOKEN_FORCE_PROCESS_SYSTEM_INCLUDE_PATHS={extra_include_paths}")
+ log.info(f"Shiboken will now process system headers from: {extra_include_paths}")
+
cmake_cmd += [
"-G", self.make_generator,
f"-DBUILD_TESTS={self.build_tests}",
cmake_cmd += platform_cmake_options()
- if sys.platform == 'darwin':
+ # for a macOS host, cross-compilation is possible, but for the host system as such
+ # we only build shiboken. Hence the following code can be skipped.
+ if sys.platform == 'darwin' and not self.is_cross_compile:
if OPTION["MACOS_ARCH"]:
# also tell cmake which architecture to use
cmake_cmd.append(f"-DCMAKE_OSX_ARCHITECTURES:STRING={OPTION['MACOS_ARCH']}")
f"folder as {basename}.")
destination_path = destination_dir / basename
+ # It is possible that the resolved libclang has a different SONAME
+ # For example the actual libclang might be named libclang.so.14.0.0 and its
+ # SONAME might be libclang.so.13
+ # In this case, the ideal approach is to find the SONAME and create a symlink to the
+ # actual libclang in the destination directory. But, Python packaging (setuptools)
+ # does not support symlinks.
+ # So, we rename the actual libclang to the SONAME and copy it to the destination
+ if sys.platform == 'linux':
+ soname = get_soname(clang_lib_path)
+ if soname and soname != clang_lib_path.name:
+ destination_path = destination_path.parent / soname
+
# Need to modify permissions in case file is not writable
# (a reinstall would cause a permission denied error).
copyfile(clang_lib_path,
('plat-name=', None, 'The platform name for which we are cross-compiling'),
('unity', None, 'Use CMake UNITY_BUILD_MODE (obsolete)'),
('no-unity', None, 'Disable CMake UNITY_BUILD_MODE'),
- ('unity-build-batch-size=', None, 'Value of CMAKE_UNITY_BUILD_BATCH_SIZE')
+ ('unity-build-batch-size=', None, 'Value of CMAKE_UNITY_BUILD_BATCH_SIZE'),
+ # shiboken-force-process-system-headers option is specifically used to tell the clang
+ # inside shiboken to process the system headers, when building against a system Qt.
+ #
+ # This option is specific for Flatpak and OS distro builds of PySide6. So, use with
+ # caution as it may also try to parse other global headers.
+ ('shiboken-force-process-system-headers', None,
+ 'When building PySide against system Qt, shiboken does not ignore the system Qt headers'),
+ # shiboken-extra-inlude-paths option is specifically used to tell the clang inside shiboken
+ # to include extra paths when parsing the headers. Use with caution.
+ ('shiboken-extra-include-paths=', None,
+ 'Extra include paths for shiboken. Comma separated.'),
+ # flatpak option is used to build PySide6 for Flatpak. Flatpak is a special case where
+ # some of the headers for the Qt modules are located as system headers in /usr/include in
+ # the KDE flatpak SDK. Therefore --shiboken-force-process-system headers will be by
+ # default enabled when --flatpak is enabled.
+ # Apart from that, headers for certain Qt modules like QtWebEngine, QtPdf etc. are located
+ # in /app/include from the Flapak WebEngine baseapp. Therefore when the --flatpak option is
+ # enabled, the extra include path of /app/include will be added to the option
+ # --shiboken-extra-include-paths.
+ ('flatpak', None, 'Build PySide6 for Flatpak.'),
]
def __init__(self):
self.unity = False
self.no_unity = False
self.unity_build_batch_size = "16"
+ self.shiboken_force_process_system_headers = False
+ self.shiboken_extra_include_paths = None
+ self.flatpak = False
# When initializing a command other than the main one (so the
# first one), we need to copy the user options from the main
"Unity build mode is now the default.")
OPTION['UNITY'] = not self.no_unity
OPTION['UNITY_BUILD_BATCH_SIZE'] = self.unity_build_batch_size
+ OPTION['SHIBOKEN_FORCE_PROCESS_SYSTEM_HEADERS'] = self.shiboken_force_process_system_headers
+ OPTION['SHIBOKEN_EXTRA_INCLUDE_PATHS'] = self.shiboken_extra_include_paths
+ OPTION['FLATPAK'] = self.flatpak
+ if OPTION['FLATPAK']:
+ OPTION['SHIBOKEN_FORCE_PROCESS_SYSTEM_HEADERS'] = True
+ OPTION['SHIBOKEN_EXTRA_INCLUDE_PATHS'] = '/app/include'
qtpaths_abs_path = None
if self.qtpaths and Path(self.qtpaths).exists():
macos_add_rpath(rpath, binary)
-def prepare_standalone_package_macos(pyside_build, _vars):
+def prepare_standalone_package_macos(pyside_build, _vars, is_android=False):
built_modules = _vars['built_modules']
constrain_modules = None
ignored_modules = []
if not pyside_build.is_webengine_built(built_modules):
ignored_modules.extend(['libQt6WebEngine*.dylib'])
+
accepted_modules = ['libQt6*.6.dylib']
+ if is_android:
+ accepted_modules = ['libQt6*.so', '*-android-dependencies.xml']
+
if constrain_modules:
accepted_modules = [f"libQt6{module}*.6.dylib" for module in constrain_modules]
# <qt>/plugins/* -> <setup>/{st_package_name}/Qt/plugins
plugins_target = destination_qt_dir / "plugins"
filters = ["*.dylib"]
+ if is_android:
+ filters = ["*.so"]
copydir("{qt_plugins_dir}", plugins_target,
_filter=filters,
recursive=True,
script_dirs = ["qtpy2cpp_lib", "deploy_lib", "project"]
- if sys.platform.startswith("linux"):
+ if sys.platform in ["linux", "darwin"]:
scripts.append("android_deploy.py")
scripts.append("requirements-android.txt")
script_dirs.extend(["deploy_lib/android",
if config.is_internal_pyside_build() or config.is_internal_shiboken_generator_build():
_vars['built_modules'] = generated_config['built_modules']
if sys.platform == 'darwin':
- prepare_standalone_package_macos(pyside_build, _vars)
+ prepare_standalone_package_macos(pyside_build, _vars, is_android=is_android)
else:
prepare_standalone_package_linux(pyside_build, _vars, cross_build,
is_android=is_android)
with tempfile.TemporaryDirectory() as temp_path:
redist_url = "https://download.qt.io/development_releases/prebuilt/vcredist/"
- zip_file = "pyside_qt_deps_64_2019.7z"
- if "{target_arch}".format(**_vars) == "32":
- zip_file = "pyside_qt_deps_32_2019.7z"
+ zip_file = "pyside_qt_deps_673_64_2019.7z"
try:
download_and_extract_7z(redist_url + zip_file, temp_path)
except Exception as e:
# <qt>/bin/*.dll and Qt *.exe -> <setup>/{st_package_name}
qt_artifacts_permanent = [
- "avcodec-60.dll",
- "avformat-60.dll",
- "avutil-58.dll",
- "swresample-4.dll",
- "swscale-7.dll",
+ "avcodec-*.dll",
+ "avformat-*.dll",
+ "avutil-*.dll",
+ "swresample-*.dll",
+ "swscale-*.dll",
"opengl*.dll",
"designer.exe",
"linguist.exe",
"""
Config file handling, cache and read function
"""
-config_dict = {}
+config_dict: dict = {}
def read_config_file(file_name):
def get_config_file(base_name) -> Path:
global user
- home = os.getenv('HOME')
+ home = os.getenv('HOME', default="")
if IS_WINDOWS:
# Set a HOME variable on Windows such that scp. etc.
# feel at home (locating .ssh).
if not home:
- home = os.getenv('HOMEDRIVE') + os.getenv('HOMEPATH')
+ home = os.getenv('HOMEDRIVE', default="") + os.getenv('HOMEPATH', default="")
os.environ['HOME'] = home
user = os.getenv('USERNAME')
- config_file = Path(os.getenv('APPDATA')) / base_name
+ config_file = Path(os.getenv('APPDATA', default="")) / base_name
else:
user = os.getenv('USER')
config_dir = Path(home) / '.config'
acceleration = read_acceleration_config()
if not IS_WINDOWS and acceleration == Acceleration.INCREDIBUILD:
arguments.append(INCREDIBUILD_CONSOLE)
- arguments.appendh('--avoid') # caching, v0.96.74
+ arguments.append('--avoid') # caching, v0.96.74
arguments.extend([read_config_python_binary(), 'setup.py', target])
build_arguments = read_config_build_arguments()
if opt_verbose and LOG_LEVEL_OPTION in build_arguments:
if 'site-' in p:
numpy = Path(p).resolve() / 'numpy'
if numpy.is_dir():
- return os.fspath(numpy / 'core' / 'include')
+ candidate = numpy / '_core' / 'include' # Version 2
+ if not candidate.is_dir():
+ candidate = numpy / 'core' / 'include' # Version 1
+ if candidate.is_dir():
+ return os.fspath(candidate)
+ log.warning(f"Cannot find numpy include dir under {numpy}")
return None
module_QtHttpServer(),
module_QtLocation(),
module_QtAsyncio(),
+ module_QtWebView(),
]
return files
"libQt6QuickTimelineBlendTrees",
]
- # Adding GraphicalEffects files
- data.qml.append("Qt5Compat/GraphicalEffects")
-
data.qtlib.extend(_qtlib)
data.metatypes.extend(_metatypes)
json_data = get_module_json_data("Quick")
data.extra_files.append("qsb*")
data.extra_files.append("balsam*")
+ # Adding GraphicalEffects files
+ data.qml.append("Qt5Compat/GraphicalEffects")
+
return data
data.translations.append("qtmultimedia_*")
data.plugins = get_module_plugins(json_data)
- if sys.platform == "win32":
- data.extra_files.extend(["avcodec-60.dll", "avformat-60.dll", "avutil-58.dll",
- "swresample-4.dll", "swscale-7.dll"])
+ platform_files = {
+ "win32": ["avcodec-*.dll", "avformat-*.dll", "avutil-*.dll", "swresample-*.dll",
+ "swscale-*.dll"],
+ "darwin": [f"Qt/lib/{dependency_lib}" for dependency_lib in ["libavcodec.*.dylib",
+ "libavformat.*.dylib",
+ "libavutil.*.dylib",
+ "libswresample.*.dylib",
+ "libswscale.*.dylib"]]}
+
+ extra_files = platform_files.get(sys.platform, [])
+ data.extra_files.extend(extra_files)
return data
def module_QtExampleIcons() -> ModuleData:
data = ModuleData("ExampleIcons")
return data
+
+
+def module_QtWebView() -> ModuleData:
+ data = ModuleData("WebView")
+ return data
def finalize_options(self):
CommandMixin.mixin_finalize_options(self)
- if sys.platform == 'darwin':
+ if sys.platform == 'darwin' and not self.is_cross_compile:
# Override the platform name to contain the correct
# minimum deployment target.
# This is used in the final wheel name.
product_dependency:
../../qt/qt5:
- ref: "3f005f1e2e88485dbf541200ba3fafcad6ea84ad"
+ ref: "90e86aee3dd3fdc502e65d5bf2916871ae9168fe"
dependency_source: supermodule
dependencies: [
"../../qt/qt3d",
"../../qt/qtwayland",
"../../qt/qtwebchannel",
"../../qt/qtwebengine",
- "../../qt/qtwebsockets"
+ "../../qt/qtwebsockets",
+ "../../qt/qtwebview",
]
--- /dev/null
+#!/bin/bash
+# Copyright (C) 2024 The Qt Company Ltd.
+# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+# Download the file
+wget -q https://download.qt.io/development_releases/prebuilt/libclang/libclang-release_18.1.7-based-linux-Debian-11.6-gcc10.2-arm64.7z
+if [ $? -ne 0 ]; then
+ echo "Error: Failed to download libclang archive" >&2
+ exit 1
+fi
+
+# Unzip the contents to /home/qt
+7z x libclang-release_18.1.7-based-linux-Debian-11.6-gcc10.2-arm64.7z -o/home/qt
+if [ $? -ne 0 ]; then
+ echo "Error: Failed to extract libclang archive" >&2
+ exit 1
+fi
+
+# Remove the 7z file after extraction
+rm libclang-release_18.1.7-based-linux-Debian-11.6-gcc10.2-arm64.7z
+if [ $? -ne 0 ]; then
+ echo "Error: Failed to remove libclang archive" >&2
+ exit 1
+fi
variableValue: "/Users/qt/.local/bin/:"
enable_if:
condition: property
- property: host.osVersion
- equals_value: MacOS_11_00
+ property: host.os
+ equals_value: MacOS
- type: PrependToEnvironmentVariable
variableName: PATH
variableValue: "/Users/qt/work/install/bin:"
property: host.os
equals_value: MacOS
- type: ExecuteCommand
- command: "sudo apt-get install python3-pip libclang-11-dev clang -y"
+ command: "sudo apt-get install python3-pip -y"
maxTimeInSeconds: 14400
maxTimeBetweenOutput: 1200
enable_if:
userMessageOnFailure: >
Failed to install dependencies
- type: ExecuteCommand
- command: "python3 -m pip install -U setuptools==69.1.1"
+ command: "chmod +x coin/fetch_libclang_arm64.sh"
maxTimeInSeconds: 14400
maxTimeBetweenOutput: 1200
enable_if:
property: host.arch
equals_value: AARCH64
userMessageOnFailure: >
- Failed to install setuptools
+ Failed to make coin/fetch_libclang_arm64.sh executable
+ - type: ExecuteCommand
+ command: "coin/fetch_libclang_arm64.sh"
+ maxTimeInSeconds: 14400
+ maxTimeBetweenOutput: 1200
+ enable_if:
+ condition: and
+ conditions:
+ - condition: property
+ property: host.os
+ equals_value: Linux
+ - condition: property
+ property: host.arch
+ equals_value: AARCH64
+ userMessageOnFailure: >
+ Failed to download libclang from Qt servers
- type: EnvironmentVariable
variableName: LLVM_INSTALL_DIR
- variableValue: "/usr/lib/llvm-11/lib"
+ variableValue: "/home/qt/libclang"
enable_if:
condition: and
conditions:
- condition: property
property: host.os
equals_value: Linux
+ - type: ExecuteCommand
+ command: "python3 -m pip install -r requirements-coin.txt"
+ maxTimeInSeconds: 14400
+ maxTimeBetweenOutput: 1200
+ enable_if:
+ condition: and
+ conditions:
+ - condition: property
+ property: host.os
+ equals_value: Linux
+ - condition: property
+ property: host.arch
+ equals_value: AARCH64
+ userMessageOnFailure: >
+ Failed to install requirements-coin.txt on Linux (aarch64)
- type: EnvironmentVariable
variableName: interpreter
variableValue: "python3.11"
type: Group
instructions:
- type: ExecuteCommand
- command: "python3 -m pip install -U setuptools==69.1.1"
+ command: "python3 -m pip install -r requirements-coin.txt"
maxTimeInSeconds: 14400
maxTimeBetweenOutput: 1200
enable_if:
property: host.os
equals_value: MacOS
userMessageOnFailure: >
- Failed to install setuptools on macOS
+ Failed to install requirements-coin.txt on macOS
- type: ExecuteCommand
command: "python3 -u coin_build_instructions.py --os={{.Env.CI_OS}} {{.Env.CI_PACKAGING_FEATURE}} {{.Env.CI_USE_SCCACHE}} --instdir=/Users/qt/work/install --targetOs={{.Env.CI_OS}} --hostArch=X86_64 --targetArch={{.Env.CI_TARGET_ARCHITECTURE}} --phase=ALL"
maxTimeInSeconds: 14400
property: host.os
equals_value: Windows
- type: ExecuteCommand
- command: "{{.Env.interpreter}} -m pip install -U pip setuptools==69.1.1 --user"
+ command: "{{.Env.interpreter}} -m pip install -r requirements-coin.txt --user"
maxTimeInSeconds: 14400
maxTimeBetweenOutput: 1200
enable_if:
property: host.os
equals_value: Linux
userMessageOnFailure: >
- Failed to install setuptools on Linux
+ Failed to install requirements-coin.txt on Linux
- type: ExecuteCommand
command: "{{.Env.interpreter}} -u coin_build_instructions.py --os={{.Env.CI_OS}} {{.Env.CI_PACKAGING_FEATURE}} {{.Env.CI_USE_SCCACHE}} --instdir=/home/qt/work/install --targetOs={{.Env.CI_OS}} --hostArch={{.Env.HOST_ARCH_COIN}} --targetArch={{.Env.TARGET_ARCH_COIN}}--phase=ALL"
maxTimeInSeconds: 14400
userMessageOnFailure: >
Failed to execute build instructions on Linux
- type: ExecuteCommand
- command: "c:\\users\\qt\\MSVC.bat {{.Env.PYTHON3_PATH}}\\python.exe -m pip install -U setuptools==69.1.1"
+ command: "c:\\users\\qt\\MSVC.bat {{.Env.PYTHON3_PATH}}\\python.exe -m pip install -r requirements-coin.txt"
maxTimeInSeconds: 14400
maxTimeBetweenOutput: 1200
enable_if:
property: host.os
equals_value: Windows
userMessageOnFailure: >
- Failed to install setuptools on Windows
+ Failed to install requirements-coin.txt on Windows
- type: ExecuteCommand
command: "c:\\users\\qt\\MSVC.bat {{.Env.PYTHON3_PATH}}\\python.exe -u coin_build_instructions.py --os={{.Env.CI_OS}} {{.Env.CI_PACKAGING_FEATURE}} {{.Env.CI_USE_SCCACHE}} --instdir=\\Users\\qt\\work\\install --targetOs={{.Env.CI_OS}} --hostArch=X86_64 --targetArch={{.Env.CI_TARGET_ARCHITECTURE}} --phase=BUILD"
maxTimeInSeconds: 14400
not_contains_value: LicenseCheck
instructions:
- type: ExecuteCommand
- command: "python3 -m pip install -U setuptools==69.1.1"
+ command: "python3 -m pip install -r requirements-coin.txt"
maxTimeInSeconds: 14400
maxTimeBetweenOutput: 1200
enable_if:
property: host.os
equals_value: MacOS
userMessageOnFailure: >
- Failed to install setuptools on macOS
+ Failed to install requirements-coin.txt on macOS
- type: ExecuteCommand
command: "python3 -u coin_test_instructions.py --os={{.Env.CI_OS}} {{.Env.CI_PACKAGING_FEATURE}} --instdir=/Users/qt/work/install --targetOs={{.Env.CI_OS}} --hostArch=ARM64 --targetArch={{.Env.CI_TARGET_ARCHITECTURE}}"
maxTimeInSeconds: 14400
condition: and
conditions:
- condition: property
- property: host.osVersion
- equals_value: MacOS_11_00
+ property: host.os
+ equals_value: MacOS
- condition: property
property: host.arch
equals_value: ARM64
userMessageOnFailure: >
Failed to execute test instructions on macOS
- type: ExecuteCommand
- command: "{{.Env.interpreter}} -m pip install -U pip setuptools==69.1.1 --user"
+ command: "{{.Env.interpreter}} -m pip install -r requirements-coin.txt --user"
maxTimeInSeconds: 14400
maxTimeBetweenOutput: 1200
enable_if:
property: host.os
equals_value: Linux
userMessageOnFailure: >
- Failed to install setuptools on Linux
+ Failed to install requirements-coin.txt on Linux
- type: ExecuteCommand
command: "{{.Env.interpreter}} -u coin_test_instructions.py --os={{.Env.CI_OS}} {{.Env.CI_PACKAGING_FEATURE}} --instdir=/home/qt/work/install --targetOs={{.Env.CI_OS}} --hostArch=X86_64 --targetArch={{.Env.CI_TARGET_ARCHITECTURE}}"
maxTimeInSeconds: 14400
userMessageOnFailure: >
Failed to execute test instructions on Linux
- type: ExecuteCommand
- command: "c:\\users\\qt\\MSVC.bat {{.Env.PYTHON3_PATH}}\\python.exe -m pip install -U pip setuptools==69.1.1 --user"
+ command: "c:\\users\\qt\\MSVC.bat {{.Env.PYTHON3_PATH}}\\python.exe -m pip install -r requirements-coin.txt --user"
maxTimeInSeconds: 14400
maxTimeBetweenOutput: 1200
enable_if:
property: host.os
equals_value: Windows
userMessageOnFailure: >
- Failed to install setuptools on Windows
+ Failed to install requirements-coin.txt on Windows
- type: ExecuteCommand
command: "c:\\users\\qt\\MSVC.bat {{.Env.PYTHON3_PATH}}\\python.exe -u coin_test_instructions.py --os={{.Env.CI_OS}} {{.Env.CI_PACKAGING_FEATURE}} --instdir=c:\\Users\\qt\\work\\install --targetOs={{.Env.CI_OS}} --hostArch=X86_64 --targetArch={{.Env.CI_TARGET_ARCHITECTURE}}"
maxTimeInSeconds: 14400
- condition: property
property: target.os
not_contains_value: IOS
- - condition: and
- conditions:
- condition: property
property: host.osVersion
- equals_value: MacOS_11_00
- - condition: property
- property: host.arch
- equals_value: ARM64
- - condition: property
- property: features
- contains_value: TestOnly
- - condition: property
- property: features
- contains_value: Packaging
+ not_equals_value: MacOS_12
- condition: and # Restore LoA config
conditions:
- condition: property
@dataclass
class SetupData:
name: str
- version: str
+ version: tuple[str, str]
description: str
readme: str
console_scripts: List[str]
-def get_version_from_package(name: str, package_path: Path) -> str:
+def get_version_from_package(name: str, package_path: Path) -> tuple[str, str]:
# Get version from the already configured '__init__.py' file
version = ""
with open(package_path / name / "__init__.py") as f:
module_name = config_py.name[:-3]
_spec = importlib.util.spec_from_file_location(f"{module_name}", config_py)
+ if _spec is None:
+ raise RuntimeError(f"Unable to create ModuleSpec from {str(config_py)}")
_module = importlib.util.module_from_spec(_spec)
+ if _spec.loader is None:
+ raise RuntimeError(f"ModuleSpec for {module_name} has no valid loader.")
_spec.loader.exec_module(module=_module)
target = _module.__qt_macos_min_deployment_target__
_pyside_tools = available_pyside_tools(packaged_qt_tools_path, package_for_wheels=True)
# replacing pyside6-android_deploy by pyside6-android-deploy for consistency
- # Also, the tool should not exist in any other platform than Linux
+ # Also, the tool should not exist in any other platform than Linux and macOS
_console_scripts = []
- if ("android_deploy" in _pyside_tools) and sys.platform.startswith("linux"):
+ if ("android_deploy" in _pyside_tools) and sys.platform in ("linux", "darwin"):
_console_scripts = ['pyside6-android-deploy = "PySide6.scripts.pyside_tool:android_deploy"']
_pyside_tools.remove("android_deploy")
--- /dev/null
+Qt for Python 6.7.3 is a bug-fix release.
+
+For more details, refer to the online documentation included in this
+distribution. The documentation is also available online:
+
+https://doc.qt.io/qtforpython/
+
+Some of the changes listed in this file include issue tracking numbers
+corresponding to tasks in the Qt Bug Tracker:
+
+https://bugreports.qt.io/
+
+Each of these identifiers can be entered in the bug tracker to obtain more
+information about a particular change.
+
+****************************************************************************
+* PySide6 *
+****************************************************************************
+
+ - A --flatpak option has been added to setup.py, enabling a flatpak build
+ of Qt for Python.
+ - [PYSIDE-769] QtAsyncio: The application argument has been removed
+ from the loop policy.
+ - [PYSIDE-1612] Deployment: Nuitka has been updated to 2.3.7. Scanning for
+ QML dependencies has been fixed to skip some directories.
+ - [PYSIDE-1612] Android Cross Compilation: INSTSONAME has been added.
+ - [PYSIDE-1877] Properties of type QAbstractItemModel can now be used in QML.
+ - [PYSIDE-2192] PySide Qt Gui applications can now be used in interactive
+ mode, for example notebooks.
+ - [PYSIDE-2517] Type hints: The signatures of QObject.findChild()/
+ findChildren() have been improved to reflect the type passed
+ in.
+ - [PYSIDE-2622] Deployment: Nuitka --standalone mode is now supported.
+ - [PYSIDE-2656] QtMultimedia on macOS has been fixed.
+ - [PYSIDE-2702] An option to force processing system headers has been added
+ for cases where Qt is installed into the system.
+ - [PYSIDE-2752] Type hints: A syntax error caused by empty Enums has been
+ fixed.
+ - [PYSIDE-2766] Android Deployment: pyside6-android-deploy now works macOS,
+ too.
+ - [PYSIDE-2785] Deployment: 'dist-packages' is now skipped similar
+ to 'site-packages' when scanning for QML dependencies.
+ - [PYSIDE-2788] Type hints: The signature of QFormLayout.getLayoutPosition()
+ has been fixed.
+ - [PYSIDE-2789] numpy 2.0 is now supported.
+ - [PYSIDE-2790] QtAsyncio: cancel count and uncancel() have been added.
+ - [PYSIDE-2796] A potential crash in currentOpcode_Is_CallMethNoArgs()
+ has been fixed.
+ - [PYSIDE-2799] QtAsyncio: A hang when an exception occurs inside a
+ TaskGroup body has been fixed.
+ - [PYSIDE-2803] Desktop Deployment: Overflows of command lines on Windows
+ have been fixed.
+ - [PYSIDE-2806] Desktop Deployment: The application name has been fixed.
+ - [PYSIDE-2814] Deployment: Arguments with spaces can now be used for
+ "extra_args" due to using shlex for splitting the command
+ line arguments.
+ - [PYSIDE-2819] The correct libclang is now used for arm64.
+ - [PYSIDE-2825] QtWebView has been added.
+ - [PYSIDE-2828] Documentation: The .qrc tutorial has been updated.
+ - [PYSIDE-2833] The QML tutorials have been updated.
+ - [PYSIDE-2834] QDir.entry(Info)List(QDir.Filter, QDir.SortFlags)
+ has been fixed to work with Python 3.11 and later.
+ - [PYSIDE-2836] PySide6/__init__.py now has a static list of modules,
+ enabling code checkers to work.
+ - [PYSIDE-2870] A crash when using QStateMachine.postEvent() has been
+ fixed.
+
+****************************************************************************
+* Shiboken6 *
+****************************************************************************
+
+- [PYSIDE-2834] Enumerations have been excluded from argument type checks
+ for sequences. This addresses a problem showing in Python
+ 3.11 causing the wrong function overloads to be used.
+- [PYSIDE-2780] A potential refcounting bug in Lazy loading has been fixed.
--- /dev/null
+{
+ "files": ["main.py"]
+}
To build:
.. code-block:: bash
+
ninja
ninja install
cd ..
# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
-import functools
from enum import IntEnum
from PySide6.QtCore import QUrl, Slot
from PySide6.QtGui import QStandardItemModel, QStandardItem
from PySide6.QtWidgets import QMainWindow
-from PySide6.QtSerialBus import (QModbusDataUnit,
- QModbusDevice, QModbusReply,
+from PySide6.QtSerialBus import (QModbusDataUnit, QModbusDevice,
QModbusRtuSerialClient, QModbusTcpClient)
from ui_mainwindow import Ui_MainWindow
elif index == ModbusConnection.TCP:
self._modbus_device = QModbusTcpClient(self)
if not self.ui.portEdit.text():
- self.ui.portEdit.setText("127.0.0.1:502")
+ self.ui.portEdit.setText("127.0.0.1:50200")
self._modbus_device.errorOccurred.connect(self._show_device_errorstring)
self.ui.serverEdit.value())
if reply:
if not reply.isFinished():
- reply.finished.connect(functools.partial(self.onReadReady, reply))
+ reply.finished.connect(self.onReadReady)
else:
del reply # broadcast replies return immediately
else:
self.statusBar().showMessage(message, 5000)
@Slot()
- def onReadReady(self, reply):
+ def onReadReady(self):
+ reply = self.sender()
if not reply:
return
# broadcast replies return immediately
reply.deleteLater()
else:
- reply.finished.connect(functools.partial(self._write_finished, reply))
+ reply.finished.connect(self._write_finished)
else:
message = "Write error: " + self._modbus_device.errorString()
self.statusBar().showMessage(message, 5000)
- @Slot(QModbusReply)
- def _write_finished(self, reply):
+ @Slot()
+ def _write_finished(self):
+ reply = self.sender()
+ if not reply:
+ return
error = reply.error()
if error == QModbusDevice.ProtocolError:
e = reply.errorString()
self.ui.serverEdit.value())
if reply:
if not reply.isFinished():
- reply.finished.connect(functools.partial(self.onReadReady, reply))
+ reply.finished.connect(self.onReadReady)
else:
del reply # broadcast replies return immediately
else:
--- /dev/null
+Minibrowser Example
+===================
+
+Simple application that demonstrates how to use a QWebView modules with Qt Quick.
+
+.. image:: minibrowser.webp
+ :width: 800
+ :alt: Minibrowser screenshot
--- /dev/null
+# Copyright (C) 2024 The Qt Company Ltd.
+# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+
+import sys
+from pathlib import Path
+
+from PySide6.QtCore import QCoreApplication, QUrl, QRect, QPoint
+from PySide6.QtGui import QGuiApplication
+from PySide6.QtQml import QQmlApplicationEngine
+from PySide6.QtWebView import QtWebView
+import argparse
+
+import rc_qml # noqa: F401
+
+
+class Utils:
+ @staticmethod
+ def fromUserInput(userInput):
+ if not userInput:
+ return QUrl.fromUserInput("about:blank")
+ result = QUrl.fromUserInput(userInput)
+ return result if result.isValid() else QUrl.fromUserInput("about:blank")
+
+
+if __name__ == "__main__":
+ QtWebView.initialize()
+ app = QGuiApplication(sys.argv)
+ QGuiApplication.setApplicationDisplayName(QCoreApplication
+ .translate("main", "QtWebView Example"))
+
+ parser = argparse.ArgumentParser(description=QGuiApplication.applicationDisplayName())
+ parser.add_argument("--url", nargs="?",
+ default="https://www.qt.io",
+ help="The initial URL to open.")
+ args = parser.parse_args()
+ initialUrl = args.url
+
+ engine = QQmlApplicationEngine()
+ context = engine.rootContext()
+ context.setContextProperty("utils", Utils())
+ context.setContextProperty("initialUrl", Utils.fromUserInput(initialUrl))
+
+ geometry = QGuiApplication.primaryScreen().availableGeometry()
+ if not QGuiApplication.styleHints().showIsFullScreen():
+ size = geometry.size() * 4 / 5
+ offset = (geometry.size() - size) / 2
+ pos = geometry.topLeft() + QPoint(offset.width(), offset.height())
+ geometry = QRect(pos, size)
+
+ engine.setInitialProperties({"x": geometry.x(), "y": geometry.y(),
+ "width": geometry.width(), "height": geometry.height()})
+ qml_file = Path(__file__).parent / "main.qml"
+ engine.load(QUrl.fromLocalFile(qml_file))
+
+ if not engine.rootObjects():
+ sys.exit(-1)
+
+ ex = app.exec()
+ del engine
+ sys.exit(ex)
--- /dev/null
+// Copyright (C) 2017 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+
+import QtQuick
+import QtQuick.Controls
+import QtWebView
+import QtQuick.Layouts
+
+
+ApplicationWindow {
+ id: window
+ visible: true
+ title: webView.title
+
+ menuBar: ToolBar {
+ id: navigationBar
+ RowLayout {
+ anchors.fill: parent
+ spacing: 0
+
+ ToolButton {
+ id: backButton
+ icon.source: "qrc:/left-32.png"
+ onClicked: webView.goBack()
+ enabled: webView.canGoBack
+ Layout.preferredWidth: navigationBar.height
+ }
+
+ ToolButton {
+ id: forwardButton
+ icon.source: "qrc:/right-32.png"
+ onClicked: webView.goForward()
+ enabled: webView.canGoForward
+ Layout.preferredWidth: navigationBar.height
+ }
+
+ ToolButton {
+ id: reloadButton
+ icon.source: webView.loading ? "qrc:/stop-32.png" : "qrc:/refresh-32.png"
+ onClicked: webView.loading ? webView.stop() : webView.reload()
+ Layout.preferredWidth: navigationBar.height
+ }
+
+ Item { Layout.preferredWidth: 5 }
+
+ TextField {
+ Layout.fillWidth: true
+ id: urlField
+ inputMethodHints: Qt.ImhUrlCharactersOnly | Qt.ImhPreferLowercase
+ text: webView.url
+ onAccepted: webView.url = utils.fromUserInput(text)
+ }
+
+ Item { Layout.preferredWidth: 5 }
+
+ ToolButton {
+ id: goButton
+ text: qsTr("Go")
+ onClicked: {
+ Qt.inputMethod.commit()
+ Qt.inputMethod.hide()
+ webView.url = utils.fromUserInput(urlField.text)
+ }
+ }
+
+ ToolButton {
+ id: settingsButton
+ icon.source: "qrc:/settings-32.png"
+ onClicked: {
+ settingsDrawer.width = (settingsDrawer.width > 0) ? 0 : window.width * 1/4
+ }
+ Layout.preferredWidth: navigationBar.height
+ }
+
+ Item { Layout.preferredWidth: 10 }
+ }
+ ProgressBar {
+ id: progress
+ anchors {
+ left: parent.left
+ top: parent.bottom
+ right: parent.right
+ leftMargin: parent.leftMargin
+ rightMargin: parent.rightMargin
+ }
+ height:3
+ z: Qt.platform.os === "android" ? -1 : -2
+ background: Item {}
+ visible: Qt.platform.os !== "ios" && Qt.platform.os !== "winrt"
+ from: 0
+ to: 100
+ value: webView.loadProgress < 100 ? webView.loadProgress : 0
+ }
+ }
+
+ Item {
+ id: settingsDrawer
+ anchors.right: parent.right
+ ColumnLayout {
+ Label {
+ text: "JavaScript"
+ }
+ CheckBox {
+ id: javaScriptEnabledCheckBox
+ text: "enabled"
+ onCheckStateChanged: webView.settings.javaScriptEnabled = (checkState == Qt.Checked)
+ }
+ Label {
+ text: "Local storage"
+ }
+ CheckBox {
+ id: localStorageEnabledCheckBox
+ text: "enabled"
+ onCheckStateChanged: webView.settings.localStorageEnabled = (checkState == Qt.Checked)
+ }
+ Label {
+ text: "Allow file access"
+ }
+ CheckBox {
+ id: allowFileAccessEnabledCheckBox
+ text: "enabled"
+ onCheckStateChanged: webView.settings.allowFileAccess = (checkState == Qt.Checked)
+ }
+ Label {
+ text: "Local content can access file URLs"
+ }
+ CheckBox {
+ id: localContentCanAccessFileUrlsEnabledCheckBox
+ text: "enabled"
+ onCheckStateChanged: webView.settings.localContentCanAccessFileUrls = (checkState == Qt.Checked)
+ }
+ }
+ }
+
+ WebView {
+ id: webView
+ url: initialUrl
+ anchors.right: settingsDrawer.left
+ anchors.left: parent.left
+ height: parent.height
+ onLoadingChanged: function(loadRequest) {
+ if (loadRequest.errorString)
+ console.error(loadRequest.errorString);
+ }
+
+ Component.onCompleted: {
+ javaScriptEnabledCheckBox.checkState = settings.javaScriptEnabled ? Qt.Checked : Qt.Unchecked
+ localStorageEnabledCheckBox.checkState = settings.localStorageEnabled ? Qt.Checked : Qt.Unchecked
+ allowFileAccessEnabledCheckBox.checkState = settings.allowFileAccess ? Qt.Checked : Qt.Unchecked
+ localContentCanAccessFileUrlsEnabledCheckBox.checkState = settings.localContentCanAccessFileUrls ? Qt.Checked : Qt.Unchecked
+ }
+ }
+}
--- /dev/null
+{
+ "files": ["main.py", "main.qml", "qml.qrc", "images/left-32.png", "images/right-32.png",
+ "images/refresh-32.png", "images/settings-32.png", "images/stop-32.png"]
+}
--- /dev/null
+<RCC>
+ <qresource prefix="/">
+ <file>main.qml</file>
+ <file alias="left-32.png">images/left-32.png</file>
+ <file alias="stop-32.png">images/stop-32.png</file>
+ <file alias="refresh-32.png">images/refresh-32.png</file>
+ <file alias="right-32.png">images/right-32.png</file>
+ </qresource>
+</RCC>
--- /dev/null
+# Resource object code (Python 3)
+# Created by: object code
+# Created by: The Resource Compiler for Qt version 6.7.2
+# WARNING! All changes made in this file will be lost!
+
+from PySide6 import QtCore
+
+qt_resource_data = b"\
+\x00\x00\x02\x7f\
+\x89\
+PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\
+\x00\x00 \x00\x00\x00 \x08\x06\x00\x00\x00szz\xf4\
+\x00\x00\x00\x06bKGD\x00i\x00\xa1\x006za\
+\x0c\x8d\x00\x00\x00\x09pHYs\x00\x00\x0b\x13\x00\x00\
+\x0b\x13\x01\x00\x9a\x9c\x18\x00\x00\x00\x07tIME\x07\
+\xdf\x01\x1a\x09+7\xecd\xf9\xf8\x00\x00\x02\x0cID\
+ATX\xc3\xcd\x97\xb1K#A\x14\xc6\xbfy\xbb\x90\
+\x85\xa4\xb0\x91\x03\x8b\x88WX\xdc),\xd8X\xad6\
+f\x09X\xab\x95r\x049\xb0\xd2\x08\xfe\x11\xa2I\xe0\
+@\x10\x11\xb1\x10\xf4\x8a\x03+1\x07G\xd8\xea@\x02\
+)\xbc+,\x84\xa4\x10\xd16\xc2.$3\x16\xee\x84\
+\xf5\xdcpF7\x93L\xb9;\xcb\xef{\xdf\xbc\xf7\xf6\
+\x0d\xd0\xe3\xc5:\xd9\xbc\xbff\x99\x86\xc7\x96\x12\x0f\x94\
+NT\x1b\xa3h\xfa\xdfk\x10\xf5a\xfd\xaa\x1e\xe7g\
+nL\x1cf\xf2N%2\x01{\xeb\x16\x19\x1e\xcb\x0e\
+]\xf2\xcd\x16\xf0\x7fK\x83\xb8\x19\xa3\x0d7&r\xcb\
+\xdb\x0e\x7f\xb3\x80\x83Uk&Y\x11\xe7\xaf\x06\x87\x08\
+\xa9\x99\xcc\xfeRp~\xb6\xdbB\xed^\x1c\xadL\xe5\
+\x93eQ|3\x1c\x00\x9a`\xc9\xb2(\x1e\xadL\xe5\
+;r\xe0\xf8\xeb\xf4\xc9\xe0\xdf\xe6\x5c\x94\xc9v\xffI\
+\xfb\xbe\xb0[\x9a\x7fiRH\xe4\x1f\xfe\xf0L\xd4\xd9\
+\x1e\xbf\x17\x9f\xed\xd9\x91\x81\x1f\x17\xd5\xf3\xb6\x0e\x1c\xac\
+Z3\xc9\xb2(v\xb3\xecj\x13,\x15\xcc\x09\x16\xcc\
+\xf6\x8fe\xd1x\xd7\x99\xbf21\xaf'\x98.\xabC\
+\x97\xcf\x0d\x8fe\xd1\x14\xa1p2\x0cL\x17;3\xa6\
+\x94J\x81\xbbnhb\x1a\x1e\xcb\x02\xd8zV\x05C\
+\x97|SU\xf7\x0b\xb2Hv\xb8\xae[\xff\x8f\x0b\xfb\
+k\x96\xd9\x12`xlI\xf5?@2\x09\x00\x12\x0f\
+\x94V-@2\x9f\x04T\x1b\xa3\xca\x05\xf8L\x92g\
+\xa2\xfc?\xec3\xa9\xd7\xf3\x00\xc9\xe6\xa0\x9c\xec3\x09\
+\x00\xea\xc3\xfa\x95j\xbed>\x09\x88\xf33\xe5\x02|\
+&\x01\x80\x1b\x13\x87\xaa\x05H&\x01@&\xefT\x94\
+\xe6\x81\x06!\xe7\xc6V\x15\xdc\x8c\xd1\x86*~\x90\xa5\
+\x07,\xc9AC\xe8\xe0\xc9]\x17\xbf,+\xb2\xe8\xdd\
+\x98\xc8\xbd\x98\x09\x97\xb7\x1d^3\x99\xdd\xed\xe8k&\
+\xb3\x83\x93\xf2\xb3\x91\xec\xf4w\xf5\xda\x9e\x1d\x19H\xdc\
+\x89\xc9n\xc0o\xc7\xa9\xb0\xf8\xcd\xd9\xe9\xab\xa14\xb4\
+\x15/\xec\x96\xe6o\xc7\xa9\x10e\xe4a\xf0\xbe\xb8\x98\
+\xf4\xf7\xd5L\xc5\xe5\xb4\xe7\xeb\x11\x07R\xed#?\x12\
+G\x0e\x00\x00\x00\x00IEND\xaeB`\x82\
+\x00\x00\x05\x15\
+\x89\
+PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\
+\x00\x00 \x00\x00\x00 \x08\x06\x00\x00\x00szz\xf4\
+\x00\x00\x00\x06bKGD\x00\xd9\x00M\x00M\x0d\x89\
+\x85\xe9\x00\x00\x00\x09pHYs\x00\x00\x0b\x13\x00\x00\
+\x0b\x13\x01\x00\x9a\x9c\x18\x00\x00\x00\x07tIME\x07\
+\xdf\x01\x17\x0f!*\x83j\xd9\xc4\x00\x00\x04\xa2ID\
+ATX\xc3\xcd\x97[L\x93g\x18\xc7\x7f_)\x05\
+Z\x0a\x149\xaa\x84\x83\xe0&\xa2\x11g\xc0L\xdc\x92\
+ZL\x06\xd9\xc6\x95\xcb\x8e\xbd\xf1b\xa0u\xa7\xbb\x1d\
+b\xdc\xc5\xee\xb69\xaa\xf4\xc6,a\xd9\x5cf\xb2\xc4\
+]\xa8\x09\x87&\x1b\x90(q\xe0\x1c\xe2\x06(8\x0f\
+\x9c\x84ZX\x81r\xe8\xbb\x0b\xfbu\xdf\xd7\x16\xf8\x98\
+K\xf4M\x9a4\xcf\xfb\xbc\xcf\xff\xff>\x87\xf7y>\
+x\xccKZ\x8brmcY6\xb0\x17\xa8\x04*\x80\
+\xbc\xe0\xd6\x10\xd0\x0e4\x03m.{\xe7\xf0\xffF\xa0\
+\xb6\xb1L\x02\xac@=P\xac\xd1n/p\x04p\xbb\
+\xec\x9d\xe2?\x13\xa8m,+\x02\xce\xae\x018\x1a\x91\
+\x1a\x97\xbd\xb3\x7f9\x05\xdd\x0a\xe0\x07\x80\xbeG\x00'\
+x\xb6/hK\xbb\x07j\x1b\xcb\x0e\x01'\xc2\xe5\xa9\
+\xa6,Js\xad\x14e\xee$;9\x1fS\x5c\x0a\x82\
+\x00\xd3s\x1e\xeez\x06\xb8~\xef\x22]C-\xcc-\
+\xceD3{\xd8e\xef<\xb9*\x81 \xdb\x1f\x94\xb2\
+\xac\xe4<^.\xad\xa3dc\x05:)\xd2i\xb3\xf3\
+>\xbc\xb3\xe3H\x92\x8e\xf8X\x13?\xffq\x86\x96\xde\
+\xefX\x0a,\x86\xab\xbe\xe2\xb2w\x9eY\x96@0\xe6\
+}\xffnJT\x96\xbcE\xf5\xf6\x83\xc4\xea\x0d*K\
+\xfe\x85Y\xda\xfb\xcfr\xf1\xc69\xeey\x06\x10<\xcc\
+5\x9d\x14C~\xfa66Z\x8a\xb8<\xd4\x84\xcf\xef\
+\x0d'\xb1Y\x99\x13RX\xb6\xf7\xc81\x97\x90x\xf3\
+\xd9O\xd8]X\x0d\x80\x10\x02I\x92\x10B08\xde\
+\xc3\xd7m\x1f3\xe9\x1bY1\x01L\x86d|\xf3\xde\
+h\x89Y\x22W\x87\xd2\x9fVe\xc2\xd5\xec<L\xf9\
+\xa6*\x84\x10\x08!B$\xfaF~\xe5xS\xdd\xaa\
+\xe0@4p91\xad\xd1\xaa\xa0^\xfe\xb3)c\x07\
+\xd6\xe2W\x91$I\xf5\xf3\xceNp\xea\x97\x0fY\x0c\
+\xcck.\x03\x83>!\x9a\xb8^E \xf8\xc2\x85n\
+\xffR\xe9\xdb\xc4\xe8bB7\x97o\x7f\xfe\xb7S\xd1\
+b\xba\xec*H\xdf\xce\xa75?\x92\x9f\xb6-\xc2\x0b\
+A\xcc\x90\x07\xf6\xca;\x99I\xb9\x14f\xecP\xb9]\
+\x08\xc1\xfc\xe2\x1c\x97n\x9e\xd7\x0c\x9e\x93\xfa\x14\xb5\xd6\
+\xcf1'\xa4R\xb7\xef\x0b6X\x8a\xc2U\xf6*\x09\
+T\x86R4kWT\x837\xc7\xaf\xb2\xb0\xe4\xd7\x04\
+\x9e\x99\x94\xcb!\xeb\x97\x18\x0df\x00\x8c\x063\x8e}\
+_\x91n\xceQ\xaaU*\x09T\xc8\xd2\x8c\xa4\x9c\x88\
+\xd8K\x92\xc4\xf8\xf4\x1dM\xe0\xa9\xa6,\x1c6'I\
+\xc6u\xaa\xf3I\xc6u\x1c\xa9tb1f\xa2\xc4\x94\
+\x09\xe4\xed\xdb\xf2\x1a\xf6=G)\xd9\xb0G\xe5~y\
+\x15f\x94b\xdfs\x14[\xf1\x1b\xcb\x82\x9b\xe3Sq\
+\xd8\x9cXL\x19\xaa\xea\x91m\xadK\xcc\xc6Q\xe9$\
+1\xce\x82\xdcICUp\xe9\xe6\x05r\xd3\xb6\x92\x91\
+\x94\xa3\x02\x97\x0de\xa7\xe4\x93\x97V\xb2b\x1e\x94\x17\
+T1t\xbf\x87\xe1\x07\x83*`\xd9\xd6\xa8\xf7/n\
+\xdd\xbfFy\xc1\x0b\x11\xcdh\xe8o\xbf\x07g\xb3\x83\
+I\xdfH\xd4\x10x|c8[\x1cL\xcfM.K\
+\xa0\xa5\xf7[\x1a;\x8e10\xd6\x1d\xd5F\xcf\xdd\x0e\
+\x1a;\x8e\xd1z\xfd\xb4<C\x84\x08\xb4\x03xfF\
+\xa9ov053\xa1\xf2\xc2\xd4\xcc\x04\xce\x16\x87\xa6\
+\xc7\x07 \xdd\xbc1B&\x84`l\xea\xb6R\xd4\xae\
+$\xd0,K\xc7\xa7o\xe3l}\x07\x9f\x7f\xea\xe1k\
+\xe6\x9f\xe2\xa4\xfb=F\xa7ni\x02\x8f\x8d\x89\xa3 \
+}{\xc8\xf5\xca\x10\xf4\x8d\x5cV\xaa6+\x09\xb4)\
+w\xeez\xfaih}\x9f\xe9\xd9I\x5c\xee\x0f\xb8=\
+\xf9\xa7\xe6\xfa//\xa8\xc2\xa0\x8f\x8f\x90\x0f\x8c]\x09\
+\xbfD[x3\xba\x16>|\x18\xf4\x09\xcc/\xcej\
+\x067\xc5%\xf3\xd1\x8b\xa7I1\xa6\xa9\xe4K\x81%\
+\x8e7\xd5qc\xecJ\xa8!\xb9\xec\x9d[\xc3{\xc1\
+\x91p\x83k\x01\xd7\xeb\x0c\x1c|\xee3R\x8ci\x11\
+\xaew\xf7~\xaf\x04Wa)\x09\xb8\x83\xad2\xa2\xa5\
+jy|\xde\xdd\xdf\xc0\xe6\xacgT\xb5\x0fpq\xe0\
+\x1cg\xbbN\x84\xb7c\xb7\xa6\x81Dv\xeb\xae\xbc\xfd\
+\xdc\xf1\xf438\xfe;\x01\xb1\x14\x1aV\xd6[\x0a\xd9\
+\xbd\xa9\x9a\x8a\xa2\x1a\xe2b\x13T3\xc3\xe2\xd2\x02\xe7\
+\xae\x9e\xa2\xb9\xe7\x9b\xd0\xb0\xb2\xe2@\xb2\xd2H\x16\xa3\
+\xd3c+~\x9d\xe7\x9f>\xc0\xdc\x82\x0f!\x02$'\
+\xa4c\x8cK\x8c(\xb5\x80\x08\xd0s\xa7\x9d\x9f\xba\x1b\
+\x18\xf1\x0e\xadm$[m(\x8d\xd7\x1b\xd9\x99gc\
+\xcb\xfa\xddl\xb0\x14b\x8e\xb7 \xa1\xc3\xe7\x7f\xc0\xb0\
+w\x90\xfe\xd1.\xbao\xb9\x97{/\xb4\x0d\xa5+y\
+\xe2\x11V\xc4\xcd\x9f\xfc\x0f\x13\x80\xe0\xc1\x12\xc0\x16\xad\
+BV\x01\xb6\x05\x87\xcf\xfe'\xfa\xe3\xf4\xb1\xaf\x7f\x00\
+\x9e\xe8\x03h^\xfe7\xb1\x00\x00\x00\x00IEND\
+\xaeB`\x82\
+\x00\x00\x03?\
+\x89\
+PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\
+\x00\x00 \x00\x00\x00 \x08\x06\x00\x00\x00szz\xf4\
+\x00\x00\x00\x06bKGD\x00i\x00\xa1\x006za\
+\x0c\x8d\x00\x00\x00\x09pHYs\x00\x00\x0b\x13\x00\x00\
+\x0b\x13\x01\x00\x9a\x9c\x18\x00\x00\x00\x07tIME\x07\
+\xdf\x01\x17\x0f)\x18\x83d\x02L\x00\x00\x02\xccID\
+ATX\xc3\xcd\x97\xcfKTQ\x14\xc7?o&\x89\
+\x22P\xc8~)\xd9\xd0/$tF\x03g\xe5d\x98\
+#I?\xd0\x8d\xd2\x22'\xc5\xa2A\xe9\x0f\xe8\xc7\xa6\
+ h;\x10\x13\x96\xc6\xe0J7\x0dd\xb8\x18\x1c\xc4\
+\xb1\xcdc\xcaQ$r\x5c\xe4 \xa6\x1b\x17B\xe0F\
+}-z\xef\xf1\xe6\xbd\xf9\xf1\xde\xa8\xe9Y\xbdw\xee\
+\xb9\xf7\xfb\xe5\x9c\xef\xbd\xf7\x5c\xd8g\x13\xac\x04\xfbC\
+\xee3\x80\x07\xf0\x02\xf5\x80C\x1eZ\x04\xa6\x80\x08\x10\
+\x0b\xfa\xc4\x95]#\xe0\x0f\xb9\x05\xa0\x11\x08\x00WL\
+\xae\xfb\x03x\x02D\x83>Q*\x98\x80?\xe4\xbe\x04\
+\x84-\x00g\x22\xd2\x1a\xf4\x89\x0b\xd9\x02l9\xc0\xdb\
+\x81\xe4\x0e\xc0\x91\xe7&\xe5\xb5\xcc\x13\xf0\x87\xdc\xbd\xc0\
+\xf0.jmX^3\x7f\x09d\xb6\xc3{$\xfa\x8e\
+\xa0O\x1c\xc9J@\xaey2\xdf*\xd7+\xdb)=\
+Vn\xf0\xcf-\x7f\xe5\xe7\x8a\x98o\xfae\xad&\x04\
+\x9d\xda\xe7\xf2\xd5\xbc\xb5\xb6\x17o\xd5}\x04!=y\
+\xd3\xa9(\x03\x93\xcf\xd9\x96\xb6\xcc\x08\xb3J\xd9\x1dZ\
+\x0d4\x9a\x01o\xae\xee4\x80'R\x13\x0c\xc6^\x98\
+\x01W\x84\xd9\x98I\x84\x81\x5c\xb3\xee\xd6\xfai\xae\xee\
+4\xf8\x13\xa9\x09\x06b\xcf\xd8\xda\xde\xb4\xa2\x85@Z\
+\x09\xe4\x13\xeew\xb6\xe8;5\x8f\xb9Y\xfd\xc0\xe0\x9f\
+]\x9a\xe4\xc3\xe4S\xab\xe0\x8a\x95\x05}\xe2\x8a\x92\x01\
+\xcf\x7f\x06W1\x0f\xc9?\xdeL\x11\xb7]\x8fTp\
+A\x10\x90\xa4\x7f\xa7\xea\xfa\xc6\x1a\xdfS\xe38\xcf6\
+XB\x9cN\x8dk\x7f\xbd\xc0\x88B\xa0>\x13x\x8b\
+\xb3;Mp\xcaw\xc9\xd1R\xba</-\x81K\x92\
+D\xefP\x1a\x81zm\x06\x1c\xda\x91[\xae\x87\xb48\
+\xbb\xd5\x89\xfa\x0c\xe8\xffs\x8di\x89\xeb\xcc\xa1%P\
+\xd8].\x08;>\x1am\x9a\xfb\x5c\xb5/3\xef\x19\
+\x9b\x1dTA\x14 \xedw6Bfc\x15L%\x03\
+S@\xa5vtt\xa6\x1f@-\x85\xd6\xd67\xd6\xf8\
+\xf4-\xc0f\xe1;@\xc1T\x09D\x80\x1e}\xc4\xe8\
+L?\x82`\xa3\xc5\xd9\x95\xe6/>r\x9c\xab\xe7n\
+\xect\x1bF\xb4%\x88e\x8b\xfa\x9cx\xc7\xd8\xecG\
+C\xaa]\x15\x0d\xf4\x5c{\x8d\xddV\xb0\x8cb\x00v\
+\x80xx\xf9O][y;p\x22Sdr5\x8e\
+\xddV\xc4\xc5S5i\xfe\xd3\xc5\x0e\xca\x8a/\x90X\
+\x9a@\x92\xb6-uJA\x9f\xf8F%\x00P\xd7V\
+>\x0ftf\x9b1\xbf\x1a\xa7\xc8v\x98\xf3'\x9dF\
+\x12%\x96I\xdc\x8b\x87\x97\x7f\xe9/\xa3\xa8|Uf\
+\xb5\xf0\xf4[\x22sC\x06\xbf\xab\xa2\x81n\xcf+l\
+\x82\xddl\x9f\x18=x\x0d\xc9~\xb5d\x86\xa6T\x0e\
+\xe8\xdb\x03\xf0>=x\xcew\xc1.g\xa2#\x13\xf8\
+\xc1~\x98\xc8\xe5X\x00\xaa\x80\xa6|;$\x03p\x93\
+\xdc|.\x1c\xe8\xc7\xe9\xbe\xdb_E\x87\x0e'\xe81\
+\xfc\xef\x00\x00\x00\x00IEND\xaeB`\x82\
+\x00\x00\x04\xcf\
+\x00\
+\x00\x141x\xda\xc5X\xddS\xe36\x10\x7f\xf7_\xa1\
+\xe6\xe1&\xe9L\x1c>\xae\xd3\x99\xb4\xf4\x06B\xb9\xa3\
+\x93\x9b\x03\x02\xe5\xfa(l%VQ$#\xc9\x04z\
+\xbd\xff\xbd+\xc9vl\xd9\x0e\xa1\xf4\xa8\x9f\xa2\xdd\xf5\
+\xee\xea\xb7\x9f\xceh\x84&\x22}\x94t\x91h\xd4\x9f\
+\x0c\xd0\xde\xce\xee\x8f\xe82!\xe8\x5c\x03g\x99b\xfe\
+\x88\xa6:\x0e\x83\xd1\x08\xcd\xce\x8e?\x0f\xa74\x22\x5c\
+\x91\xe1iL\xb8\xa6sJ\xe4\x18\xe5\xb4\x0b2\x1f\x9e\
+\xeb!\xbc\xb6$2\xa2\x98\xa1O\x17\xe8hv<\xdc\
+\x1fN\x18\xce\x14\x09\x02\xbaL\x85\xd4\xa0\xfc<\xa3\xd1\
+\xadw\x0c'\x82k)\x98Z\xd3\xaf\xc9\xcd\xef\x94\xac\
+|\xc1)~\x14\x99VA\x10\x1c\xa6)\xa3\x11\xd6T\
+\xf0k\xcac\xb1B_\x02\x04\x0f\x8d\xc7he\x09\xf6\
+xO\x15\xbdad\x8c\xb4\xcc\x88\xa5<\x8c\x11\xe5T\
+\x83\x97\x9f\xed\xf9\xb1<\xffa\xcf+\x1a\xeb\xa4\xa4]\
+\x9b\x93\xa5'\xc4`U2>\xd8\xa3\xe5h\xaa\x8d\x85\
+\x95s9\xb4\xc7\xc0r\x96\x84gG\x18\x80\xba\x14\x82\
+\xc1\x8f\xdc\xc7\xc2O\x8e\xef\xe9\xc2^\x01x%\xe7B\
+\xac\xdc5+\xd2\xe6\xc1<J\x84T\xe1\x9c26F\
+)\x96\x10\x87\x9a\x80JqD\xf9b\x8cv\x82\x1a\xdd\
+\x1a\xcf\xb4\x16\xdc\xd3X\xf8q\x83\xa3['\xd0dG\
+\x82\x87Jd2\x82\x1b\xf6\xeed4\x1e12\xd7\xc3\
+\xfd\xbd0\xe5\x8b^C^\xf0\x09\x84\xe5\x96\xc4k<\
+\x16\xe2\x08\xf4\xf7\x07\x0dY\xc21\x84\xa6\x22\x19a\xfe\
+\xde\x0a7D\x1d a*\xc9\x9cHI\xe2k\x17\xa4\
+\x1a\x80a\xb2\x0eI\xf1|}&\x12s!WX\xc6\
+\xdb\x83a\x0b\xe8yh\x9c8\x1b[\x03\x92\xcb\xffo\
+\x98H\xc2\x04\xde\x0a\x92\xc2m#\x0f\x89\x88\xde\xe5 \
+)-\xd2\x02#T\x22G\xe6\x92\xa8\xe49\xd8\xad\xf5\
+\x16\x14\xa3\xb9?@k\x11\xe7l\x0b\xb6\xff\x11Z\xa7\
+\x9a,\xd1\x97.m?4\xc0%\x0f\xfa\x84\x12\x16\xb7\
+`\x9b\xeb0\xe5\x9c\xbf^\xf6(?\x04\x99dVK\
+\x93\xc9\xd3L\x7f$:\x11\xf1\x07\xca\xb5\x1aC\xa7\x0c\
+O\x97\xc9\x95d\x93\x04K\x1ci\x22\xd5'\xce\x1e\xd1\
+\xdf9\xe7\xcc\xba<\x15+\xe8\xd4X5\xcdi\xf0x\
+\x0d'\x18n\x09\xcca\x14\x91TW#\x03r\xe8\x00\
+e\x9a2\xe8OR,\xaf\x14\x91\xa7\xc6\xb7\xbe\xd1\xe7\
+E\xe3\x85\x90>\x95\xaf\xd0o\xdas\xd5]\xedN]\
+\xca~\xef\xbd\xe8\x0d6\xa5\x5cS\xb5y\x00\xc2\x0a\xe2\
+a\x04\x03\x8f\xea\x96dk\x91MhL:$\x9f\x06\
+\xb1\x88\x7f\xd8\x82\xa6\x05\xf4%\xf5\xad\x88\xd6PUj\
+\xfb\xa6W\xbc\xb1M\xed\xb6\x03Yh8\x96\x18\x121\
+\xb43\x17\xee\xdeo\xa5\xff\x82v\x06P\xf3;\xa8\x98\
+\xec9\xfd{\xb4;z\xfb\x04\x18\xafV\xfa\xbb;U\
+\xcb\x95\x9fgR,\xa0\xd1\xa9\xfa\xf8/\xd1Osv\
+\x9d\x93\xcf\xfa\x16\xf4\xcc\xf4-\xa6\x7fh\x0e\xcd4\x17\
+i)p# \xa6\xcb\x86\x88t\xabL.$\x1b7\
+/\x0c}\xc4rAy\xcd\x9c#\xb5k\xf4\xc4+\xb4\
+`S\x88\xf2\xcdj\xbfN\xfd\xcb\xf6\xb2\x94a\x0dC\
+y\x19\x0a\x85\x0e\x0e\x0eP\x0f\xf3X\x0a\x1a\xf7 !\
+\x86\xbb\x90\x11\xc3\xbd\xfakf\x97YH\x91q\xc0\xd6\
+E\xcc\xb3Vn\x84\x9e\xf6\xef\x8cv*T\x0f\xbdy\
+\xd3\xca\x83\xdc\x93\xda\xcbuS\xa6f\xdf\xf2\x02`\xd2\
+\xc1#\xdec\x96y#\xb2\xc8\x0c\xf4\xb3\x11\xafL\xb5\
+\x1a\xb3\xaa\xde]%\xcfKw\xbb\xa0\xad\x94]\xf9\x04\
+\xfe\xe6\xb8)\xec\x13\xc1\xb2%o]<\xa7\xf8\x86\xb0\
+\x96Tt\xfd\xb4\xf7\x1b\xbe\xc7\xb3H\xd2\xd4\x03\xa7\x8e\
+\xfb$!\xb0d\x8a\x87\x8e&\xf4g\xa9\xe4W\xb7\x04\
+\x15\xf2]V\xf3]\xa9\xb5\xf7\x98Wg\x1ak\x023\
+\x90/\xaas\xaa\x00(l\xd83\xdd'*_\x84\x5c\
+39`5\x91x\xb0\xe1^O\x803\x15\x11|\x0e\
+\xc1\x9a\x22\xf1\x82\xbc\x00\x1ff\xf4\xcc\x9c\x9a\xd7A\xa8\
+\xc5\xe27\xc2\xe8\x901\xf8|\x83=\x88 \x0c\x9b\x85\
+R/\xc0\x09\x1b]'\xa0\xea\xd0jz\x1d\xa8<\xa3\
+\xdf4\x95`\x1ak\xa8^\x04\x9f\x069Z\x0e\xb9\xab\
+\x8b\xa9zi~M\x9c\xee\x09\xe6\xee\x22\xe6J\xb0I\
+\xaaWL\xb8.\x17\xfe\x0d\xa6\xb5n\x99\xff\xa1\xe05\
+\xcc\xdc\x89\x92\x06\x8bV\xf9\x85\x7fU\xd9}\xbd\x0e\xea\
+\xad)\xb51\x5c\x88v\x0f\xea\xe2\x9f\x84\x9c\xe7m\x1e\
+\x82O\xddgN\x89\xd4<\xe3\x91YS\xfaf.\x5c\
+\x90\xbb\x8c(=\xf0\xe2H\xe7\xa8\xca\x0ea5\x11r\
+\xa6%\xe8i\xee\x8b\x90CJ0\xe2\x84:_\xfb)\
+hY\x84\xcc_C\x82\x1b\xb7!\xa8\xf0\x9b\x11\xdd\xdc\
+\xf2:\x9byX\x0d!\xda\xd0\x8c\xdfU\xc2\x8b\xec\xa8\
+\xbe\xe2\x91;\xd6,mh\x8b\x1d\xb6\xda\xda\xda\xb6\xd6\
+67\x97\x0e\x83~sx\xd6\xd5\xb6\xac\xc8Mw\xed\
+\xac\xa8m\x1c)J\xe8k\xf0\x0f\x96\x1c\x9e\xf1\
+\x00\x00\x036\
+\x89\
+PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\
+\x00\x00 \x00\x00\x00 \x08\x06\x00\x00\x00szz\xf4\
+\x00\x00\x00\x06bKGD\x00i\x00\xa1\x006za\
+\x0c\x8d\x00\x00\x00\x09pHYs\x00\x00\x0b\x13\x00\x00\
+\x0b\x13\x01\x00\x9a\x9c\x18\x00\x00\x00\x07tIME\x07\
+\xdf\x01\x17\x0f(,\xbb\xcb\xc7\xb8\x00\x00\x02\xc3ID\
+ATX\xc3\xcd\x97\xbfOSQ\x14\xc7?\xf7\xb5\x8b\
+\x93&j\x94\x10\xa1\x03\x1a\xa2\xa5\x96\x18\xbbH\xab\xa2\
+\x18\x194\xb0\xc8\xa4U`\xb0\x81\xf4\x0f\x908)\xc6\
+\x99\xc4\xd4 \x12\x8a\x13L\xc4\x1f\x01!\x10\xa0L\x0d\
+\x11JIc\xca\x22\x18\xa2\x83##\xf4:\xf8^\xf3\
+\xca{}?\x10\x027\xe9\xd0{\xce=\xdf\xef;\xe7\
+{\xcf\xbd\x17\x0ey\x087\xce\xb1d\xa8\x02\x08\x03M\
+@\x03\xe0SM?\x80\x05`\x0aH%\xa2\xe9_\xfb\
+F \x96\x0c\x09\xa0\x11\xe8\x03.:\x8c\x9b\x03\xe2\xc0\
+L\x22\x9a\x96{&\x10K\x86\xce\x03c.\x80\xcd\x88\
+\xb4$\xa2\xe9\xb5r\x0e\x8a\x05\xf8\x03 \xff\x1f\xe0\xa8\
+k\xf3j,\xe7\x04b\xc9P\x170\xb2\x8fZ\x1bQ\
+c\xda\x97@e;r@\xa2oKD\xd3\xa3e\x09\
+\xa85\xcf[E\xa8\xad\x08\xe1\xaf\xbcf\x98\xff\xb3\xb5\
+\xc9\xec\xf7Q'$.\xe85!v\xa9}\xd5\xae\xe6\
+\x8a\xf0\xd0\x11yI\xb0\xea\xa6\xc16\xb5\xfa\x81\xb1\xa5\
+7N\x84\xe9\xd7v\x87^\x03\x8dN\x04W\x90;\x0c\
+\xa6\x9e\x93\xd9\x983\xd8\x9a\xfc\x0fi\xa9\xefr\x22\xcc\
+F3\x11\xf69-\xe4Na\x9b\xf7\xa9\x1e2\x1bs\
+\x08!J~w\xea\x1eq\xbf>f\x17\xa2\x88\xe5\xd1\
+u\xb8\xd7n\xd4$e\x81\xe5\x9f\xb3T\x9e\xa8\xe1\xec\
+q_\x89\xad\xe6L\x10Ex\xc9\xff^,\xb7\xfc\xf4\
+\xd5\xd6\xca\xfe\xc5\xb1\xcd--\x03\xe1\xbdHz\xa7\xb0\
+\xcd\xc0\xfc32\x1bsHY\xda\xf0\x9a\x03O\xb8\x17\
+|j\xb5<\x0c\xe0\xd5\xca\xa7\xb7\xd4W\xdfrE\xe4\
+\xdb\xfa4\xd5\xa7.q\xfc\xd8\xc9\x92\xf9\xbbu\x8f\x91\
+\xb2\xc0\xe7L\xbf\xd9\xb2&`T#\xd0\xa0\xb7tF\
+z\x11B \xa5D\x08a\x92~\xe3|9\xdf\xe6@\
+;\x80\x19\x89\x06}\x06|\xe5k\xfd/\xb5\x1a!k\
+]\x98\xfb6\x07\xda\x91H\xbed\xde\xe9\xdd}\x96g\
+\x813!J[RvC\xd1\x9d\xe7\xc6>\xad\xdb^\
+V\xff\xedl\xe3+\x83\xbb\xbf\xbe\x88\xa9\x95`\x01\xa8\
+\xd5,\x03\xf3=\xae\xbe\xc2\xabxi\xbd\x12/\x8aP\
+_\x82\xf1\x95\xc1r\x22\x5c\xd0\x13\x98\x02:5\xcb\xd2\
+\xfa\xb4cp\x8f\xe2\xa53\xf2\xaad\x07h\xe0\x13\xd9\
+\xa1r\xe0\x1af\xb1\x04\xa9\xbd\xd4O\x03\x0f\x9c\x8b\x18\
+l\x13\xd9!>-\xbf\xb5Z\x9e*\x12P\xefp9\
+\xb7\xe0\x1d\xe1^.W]7\xb4\xe3\xaf\xabI;\xf0\
+\x9cvo\xd4\xef\x82\xb8[\xf0`\xf5\x0d\x83m2;\
+\xcc\xc7\xa5\x84]\x88\xb8\xd9a4\xe3$\x0b\x8a\xf0\xd0\
+\x1e~a\x00\x97R2\x99\x1dvz\x1c\xcf\x1c\xbd\x0b\
+\xc9a]\xc9\x0c\x9dPu\xe8>\x00\xf0\xee\xdd\xe0\x96\
+\xef\x82}\xceD\x9b\x19\xf8\xd1~\x98\xa8\xe5X\x03\xfc\
+\xc0m\x97}\x22\xa7\xae\xf1[\x81\x1f\x89\xc7\xe9\xa1\x8f\
+\xbfgH\x11\xb4\x13s\xe2\x92\x00\x00\x00\x00IEN\
+D\xaeB`\x82\
+"
+
+qt_resource_name = b"\
+\x00\x0b\
+\x00\x90;'\
+\x00s\
+\x00t\x00o\x00p\x00-\x003\x002\x00.\x00p\x00n\x00g\
+\x00\x0e\
+\x07\xd4\xda\x07\
+\x00r\
+\x00e\x00f\x00r\x00e\x00s\x00h\x00-\x003\x002\x00.\x00p\x00n\x00g\
+\x00\x0b\
+\x00\x80-\xa7\
+\x00l\
+\x00e\x00f\x00t\x00-\x003\x002\x00.\x00p\x00n\x00g\
+\x00\x08\
+\x08\x01Z\x5c\
+\x00m\
+\x00a\x00i\x00n\x00.\x00q\x00m\x00l\
+\x00\x0c\
+\x0fz\xe9\xa7\
+\x00r\
+\x00i\x00g\x00h\x00t\x00-\x003\x002\x00.\x00p\x00n\x00g\
+"
+
+qt_resource_struct = b"\
+\x00\x00\x00\x00\x00\x02\x00\x00\x00\x05\x00\x00\x00\x01\
+\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00>\x00\x00\x00\x00\x00\x01\x00\x00\x07\x9c\
+\x00\x00\x01\x91\x08\xc1\x9ai\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\
+\x00\x00\x01\x91\x08\xc1\x9ai\
+\x00\x00\x00\x1c\x00\x00\x00\x00\x00\x01\x00\x00\x02\x83\
+\x00\x00\x01\x91\x08\xc1\x9ai\
+\x00\x00\x00Z\x00\x01\x00\x00\x00\x01\x00\x00\x0a\xdf\
+\x00\x00\x01\x91\x08\xc1\x9ai\
+\x00\x00\x00p\x00\x00\x00\x00\x00\x01\x00\x00\x0f\xb2\
+\x00\x00\x01\x91\x08\xc1\x9ai\
+"
+
+def qInitResources():
+ QtCore.qRegisterResourceData(0x03, qt_resource_struct, qt_resource_name, qt_resource_data)
+
+def qCleanupResources():
+ QtCore.qUnregisterResourceData(0x03, qt_resource_struct, qt_resource_name, qt_resource_data)
+
+qInitResources()
"../registerwigglywidget.py",
"../wigglywidget.cpp",
"../wigglywidget.h",
- "../wigglywidget.py"]
+ "../wigglywidget.py",
+ "../CMakeLists.txt",
+ "../bindings.xml"
+ ]
}
--- /dev/null
+pip>=24.2
+setuptools==72.1.0
+importlib_metadata>=6
+importlib_resources>=5.10.2
+packaging>=24
+ordered-set>=3.1.1
+more_itertools>=8.8
+jaraco.text>=3.7
+importlib_metadata>=6
+tomli>=2.0.1
+wheel>=0.43.0
+platformdirs >= 2.6.2
-sphinx==7.2.6
-sphinx-design==0.5.0
+sphinx==7.4.7
+sphinx-design==0.6.0
sphinx-copybutton==0.5.2
-sphinx-tags==0.3.1
-sphinx-toolbox
-myst-parser==2.0.0
+sphinx-tags==0.4
+sphinx-toolbox==3.7.0
+myst-parser==3.0.1
# FIXME: Using fork in order to enable the 'collapse_navbar=True'
# option for the sphinx-theme. Upstream proposal:
# https://github.com/pradyunsg/furo/pull/748#issuecomment-1895448722
# furo==2023.9.10
furo @ git+https://github.com/cmaureir/furo@add_collapse
-graphviz==0.20
+graphviz==0.20.3
# Build dependencies
-setuptools==69.1.1
-packaging==23.2
-build==1.0.3
-wheel==0.42.0
+setuptools==72.1.0
+packaging==24.1
+build==1.2.1
+wheel==0.43.0
distro==1.9.0; sys_platform == 'linux'
patchelf==0.17.2; sys_platform == 'linux'
# 1.24.4 is the last version that supports Python 3.8
"""
+import sys
import argparse
import logging
import traceback
Linux = .bin
""")
+HELP_MODE = dedent("""
+ The mode in which the application is deployed. The options are: onefile,
+ standalone. The default value is onefile.
+
+ This options translates to the mode Nuitka uses to create the executable.
+
+ macOS by default uses the --standalone option.
+ """)
+
def main(main_file: Path = None, name: str = None, config_file: Path = None, init: bool = False,
loglevel=logging.WARNING, dry_run: bool = False, keep_deployment_files: bool = False,
- force: bool = False, extra_ignore_dirs: str = None, extra_modules_grouped: str = None):
+ force: bool = False, extra_ignore_dirs: str = None, extra_modules_grouped: str = None,
+ mode: bool = False):
logging.basicConfig(level=loglevel)
if config_file and not config_file.exists() and not main_file.exists():
config = DesktopConfig(config_file=config_file, source_file=main_file, python_exe=python.exe,
dry_run=dry_run, existing_config_file=config_file_exists,
- extra_ignore_dirs=extra_ignore_dirs)
+ extra_ignore_dirs=extra_ignore_dirs, mode=mode)
# set application name
if name:
logging.info(f"[DEPLOY]: Config file {config.config_file} created")
return
+ # If modules contain QtSql and the platform is macOS, then pyside6-deploy will not work
+ # currently. The fix ideally will have to come from Nuitka.
+ # See PYSIDE-2835
+ # TODO: Remove this check once the issue is fixed in Nuitka
+ # Nuitka Issue: https://github.com/Nuitka/Nuitka/issues/3079
+ if "Sql" in config.modules and sys.platform == "darwin":
+ print("[DEPLOY] QtSql Application is not supported on macOS with pyside6-deploy")
+ return
+
try:
# create executable
if not dry_run:
excluded_qml_plugins=config.excluded_qml_plugins,
icon=config.icon,
dry_run=dry_run,
- permissions=config.permissions)
+ permissions=config.permissions,
+ mode=config.mode)
except Exception:
print(f"[DEPLOY] Exception occurred: {traceback.format_exc()}")
finally:
parser.add_argument("--extra-modules", type=str, help=HELP_EXTRA_MODULES)
+ parser.add_argument("--mode", choices=["onefile", "standalone"], default="onefile",
+ help=HELP_MODE)
+
args = parser.parse_args()
main(args.main_file, args.name, args.config_file, args.init, args.loglevel, args.dry_run,
- args.keep_deployment_files, args.force, args.extra_ignore_dirs, args.extra_modules)
+ args.keep_deployment_files, args.force, args.extra_ignore_dirs, args.extra_modules,
+ args.mode)
EXE_FORMAT = ".bin"
DEFAULT_APP_ICON = str((Path(__file__).parent / f"pyside_icon{IMAGE_FORMAT}").resolve())
+DEFAULT_IGNORE_DIRS = ["site-packages", "deployment", ".qtcreator", "build", "dist", "tests"]
+
IMPORT_WARNING_PYSIDE = (f"[DEPLOY] Found 'import PySide6' in file {0}"
". Use 'from PySide6 import <module>' or pass the module"
" needed using --extra-modules command line argument")
# Copyright (C) 2023 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
import re
+import sys
import tempfile
import logging
import zipfile
from .. import (Config, find_pyside_modules, get_all_pyside_modules, MAJOR_VERSION)
ANDROID_NDK_VERSION = "26b"
+ANDROID_NDK_VERSION_NUMBER_SUFFIX = "10909125"
ANDROID_DEPLOY_CACHE = Path.home() / ".pyside6_android_deploy"
else:
ndk_path_temp = (ANDROID_DEPLOY_CACHE / "android-ndk"
/ f"android-ndk-r{ANDROID_NDK_VERSION}")
+ if sys.platform == "darwin":
+ ndk_path_temp = (
+ ANDROID_DEPLOY_CACHE / "android-ndk"
+ / f"AndroidNDK{ANDROID_NDK_VERSION_NUMBER_SUFFIX}.app/Contents/NDK"
+ )
if ndk_path_temp.exists():
self.ndk_path = ndk_path_temp
self._dependency_files.append(dependency_file)
logging.info("[DEPLOY] The following dependency files were found: "
- f"{*self._dependency_files,}")
+ f"{*self._dependency_files, }")
def _find_local_libs(self):
local_libs = set()
# Copyright (C) 2023 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
-
+import sys
import logging
import zipfile
from dataclasses import dataclass
'''
# TODO: Requires change if Windows platform supports Android Deployment or if we
# support host other than linux-x86_64
- return (ndk_path / "toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-readobj")
+ return (ndk_path / f"toolchains/llvm/prebuilt/{sys.platform}-x86_64/bin/llvm-readobj")
def find_lib_dependencies(llvm_readobj: Path, lib_path: Path, used_dependencies: Set[str] = None,
import json
import subprocess
import sys
+import shutil
+import tempfile
from pathlib import Path
-from typing import List
+from functools import lru_cache
+
"""
All utility functions for deployment
return command_str, output
-def run_qmlimportscanner(qml_files: List[Path], dry_run: bool):
+@lru_cache
+def run_qmlimportscanner(qml_files: tuple[Path], dry_run: bool):
"""
- Runs pyside6-qmlimportscanner to find all the imported qml modules
+ Runs pyside6-qmlimportscanner to find all the imported qml modules in project_dir
"""
- if not qml_files:
- return []
-
qml_modules = []
- cmd = ["pyside6-qmlimportscanner", "-qmlFiles"]
- cmd.extend([str(qml_file) for qml_file in qml_files])
-
- if dry_run:
- run_command(command=cmd, dry_run=True)
-
- # we need to run qmlimportscanner during dry_run as well to complete the
- # command being run by nuitka
- _, json_string = run_command(command=cmd, dry_run=False, fetch_output=True)
- json_string = json_string.decode("utf-8")
- json_array = json.loads(json_string)
- qml_modules = [item['name'] for item in json_array if item['type'] == "module"]
+ # Create a temporary directory to copy all the .qml_files
+ # TODO: Modify qmlimportscanner code in qtdeclarative to include a flag to ignore directories
+ # Then, this copy into a temporary directory can be avoided
+ # See 36b425ea8bf36d47694ea69fa7d129b6d5a2ca2d in gerrit
+ with tempfile.TemporaryDirectory() as temp_dir:
+ temp_path = Path(temp_dir)
+ # Copy only files with .qml suffix
+ for qml_file in qml_files:
+ if qml_file.suffix == ".qml":
+ shutil.copy2(qml_file.resolve(), temp_path / qml_file.name)
+
+ cmd = ["pyside6-qmlimportscanner", "-rootPath", str(temp_path)]
+
+ if dry_run:
+ run_command(command=cmd, dry_run=True)
+
+ # Run qmlimportscanner during dry_run as well to complete the command being run by nuitka
+ _, json_string = run_command(command=cmd, dry_run=False, fetch_output=True)
+ json_string = json_string.decode("utf-8")
+ json_array = json.loads(json_string)
+ qml_modules = [item['name'] for item in json_array if item['type'] == "module"]
+
return qml_modules
import sys
import configparser
import logging
+import tempfile
import warnings
from configparser import ConfigParser
from typing import List
from pathlib import Path
+from enum import Enum
from project import ProjectData
-from . import (DEFAULT_APP_ICON, find_pyside_modules, find_permission_categories,
- QtDependencyReader, run_qmlimportscanner)
+from . import (DEFAULT_APP_ICON, DEFAULT_IGNORE_DIRS, find_pyside_modules,
+ find_permission_categories, QtDependencyReader, run_qmlimportscanner)
# Some QML plugins like QtCore are excluded from this list as they don't contribute much to
# executable size. Excluding them saves the extra processing of checking for them in files
def update_config(self):
logging.info(f"[DEPLOY] Creating {self.config_file}")
- with open(self.config_file, "w+") as config_file:
- self.parser.write(config_file, space_around_delimiters=True)
+
+ # This section of code is done to preserve the formatting of the original deploy.spec
+ # file where there is blank line before the comments
+ with tempfile.NamedTemporaryFile('w+', delete=False) as temp_file:
+ self.parser.write(temp_file, space_around_delimiters=True)
+ temp_file_path = temp_file.name
+
+ # Read the temporary file and write back to the original file with blank lines before
+ # comments
+ with open(temp_file_path, 'r') as temp_file, open(self.config_file, 'w') as config_file:
+ previous_line = None
+ for line in temp_file:
+ if (line.lstrip().startswith('#') and previous_line is not None
+ and not previous_line.lstrip().startswith('#')):
+ config_file.write('\n')
+ config_file.write(line)
+ previous_line = line
+
+ # Clean up the temporary file
+ Path(temp_file_path).unlink()
def set_value(self, section: str, key: str, new_value: str, raise_warning: bool = True):
try:
def set_or_fetch(self, config_property_val, config_property_key, config_property_group="app"):
"""
- Write to config_file if 'config_property_key' is known without config_file
- Fetch and return from config_file if 'config_property_key' is unknown, but
- config_file exists
- Otherwise, raise an exception
+ Set the configuration value if provided, otherwise fetch the existing value.
+ Raise an exception if neither is available.
+
+ :param value: The value to set if provided.
+ :param key: The configuration key.
+ :param group: The configuration group (default is "app").
+ :return: The configuration value.
+ :raises RuntimeError: If no value is provided and no existing value is found.
"""
+ existing_value = self.get_value(config_property_group, config_property_key)
+
if config_property_val:
self.set_value(config_property_group, config_property_key, str(config_property_val))
return config_property_val
- elif self.get_value(config_property_group, config_property_key):
- return self.get_value(config_property_group, config_property_key)
+ elif existing_value:
+ return existing_value
else:
raise RuntimeError(
- f"[DEPLOY] No {config_property_key} specified in config file or as cli option"
+ f"[DEPLOY] No value for {config_property_key} specified in config file or as cli"
+ " option"
)
@property
qml_files_temp = None
if self.source_file and self.python_path:
if not self.qml_files:
- qml_files_temp = list(self.source_file.parent.glob("**/*.qml"))
-
- # add all QML files, excluding the ones shipped with installed PySide6
- # The QML files shipped with PySide6 gets added if venv is used,
- # because of recursive glob
- if self.python_path.parent.parent == self.source_file.parent:
- # python venv path is inside the main source dir
- qml_files_temp = list(
- set(qml_files_temp) - set(self.python_path.parent.parent.rglob("*.qml"))
- )
-
- if len(qml_files_temp) > 500:
- if "site-packages" in str(qml_files_temp[-1]):
- raise RuntimeError(
- "You are including a lot of QML files from a local virtual env."
- " This can lead to errors in deployment."
- )
- else:
+ # filter out files from DEFAULT_IGNORE_DIRS
+ qml_files_temp = [file for file in self.source_file.parent.glob("**/*.qml")
+ if all(part not in file.parts for part in
+ DEFAULT_IGNORE_DIRS)]
+
+ if len(qml_files_temp) > 500:
warnings.warn(
"You seem to include a lot of QML files. This can lead to errors in "
"deployment."
if qml_files_temp:
extra_qml_files = [Path(file) for file in qml_files_temp]
self.qml_files.extend(extra_qml_files)
+
if self.qml_files:
self.set_value(
"qt",
def _find_and_set_excluded_qml_plugins(self):
if self.qml_files:
- self.qml_modules = set(run_qmlimportscanner(qml_files=self.qml_files,
+ self.qml_modules = set(run_qmlimportscanner(qml_files=tuple(self.qml_files),
+ # tuple is needed to make it hashable
dry_run=self.dry_run))
self.excluded_qml_plugins = EXCLUDED_QML_PLUGINS.difference(self.qml_modules)
"""Identify if QtQuick is used in QML files and add them as dependency
"""
extra_modules = []
- if not self.qml_modules:
+ if not self.qml_modules and self.qml_files:
self.qml_modules = set(run_qmlimportscanner(qml_files=self.qml_files,
dry_run=self.dry_run))
class DesktopConfig(Config):
"""Wrapper class around pysidedeploy.spec, but specific to Desktop deployment
"""
+ class NuitkaMode(Enum):
+ ONEFILE = "onefile"
+ STANDALONE = "standalone"
+
def __init__(self, config_file: Path, source_file: Path, python_exe: Path, dry_run: bool,
- existing_config_file: bool = False, extra_ignore_dirs: List[str] = None):
+ existing_config_file: bool = False, extra_ignore_dirs: List[str] = None,
+ mode: str = "onefile"):
super().__init__(config_file, source_file, python_exe, dry_run, existing_config_file,
extra_ignore_dirs)
self.dependency_reader = QtDependencyReader(dry_run=self.dry_run)
else:
self._find_and_set_permissions()
+ self._mode = self.NuitkaMode.ONEFILE
+ if self.get_value("nuitka", "mode") == self.NuitkaMode.STANDALONE.value:
+ self._mode = self.NuitkaMode.STANDALONE
+ elif mode == self.NuitkaMode.STANDALONE.value:
+ self.mode = self.NuitkaMode.STANDALONE
+
@property
def qt_plugins(self):
return self._qt_plugins
self._permissions = permissions
self.set_value("nuitka", "macos.permissions", ",".join(permissions))
+ @property
+ def mode(self):
+ return self._mode
+
+ @mode.setter
+ def mode(self, mode: NuitkaMode):
+ self._mode = mode
+ self.set_value("nuitka", "mode", mode.value)
+
def _find_dependent_qt_modules(self):
"""
Given pysidedeploy_config.modules, find all the other dependent Qt modules.
python_path =
# python packages to install
-# ordered-set: increase compile time performance of nuitka packaging
-# zstandard: provides final executable size optimization
-packages = Nuitka==2.3.2
+packages = Nuitka==2.4.8
# buildozer: for deploying Android application
android_packages = buildozer==1.5.0,cython==0.29.33
# eg: NSCameraUsageDescription:CameraAccess
macos.permissions =
+# mode of using Nuitka. Accepts standalone or onefile. Default is onefile.
+mode = onefile
+
# (str) specify any extra nuitka arguments
# eg: extra_args = --show-modules --follow-stdlib
extra_args = --quiet --noinclude-qt-translations
from typing import List, Set
from functools import lru_cache
-from . import IMPORT_WARNING_PYSIDE, run_command
+from . import IMPORT_WARNING_PYSIDE, DEFAULT_IGNORE_DIRS, run_command
@lru_cache(maxsize=None)
"""Finds and returns all the Python files in the project
"""
py_candidates = []
- ignore_dirs = ["__pycache__", "env", "venv", "deployment"]
+ ignore_dirs = ["__pycache__", *DEFAULT_IGNORE_DIRS]
if project_data:
py_candidates = project_data.python_files
all_modules = set()
mod_pattern = re.compile("PySide6.Qt(?P<mod_name>.*)")
+ @lru_cache
def pyside_module_imports(py_file: Path):
modules = []
try:
"""
Finds the path to the Qt libs directory inside PySide6 package installation
"""
+ # PYSIDE-2785 consider dist-packages for Debian based systems
for possible_site_package in site.getsitepackages():
- if possible_site_package.endswith("site-packages"):
+ if possible_site_package.endswith(("site-packages", "dist-packages")):
self.pyside_install_dir = Path(possible_site_package) / "PySide6"
+ if self.pyside_install_dir.exists():
+ break
if not self.pyside_install_dir:
- print("Unable to find site-packages. Exiting ...")
+ print("Unable to find where PySide6 is installed. Exiting ...")
sys.exit(-1)
if sys.platform == "win32":
from pathlib import Path
from . import EXE_FORMAT
-from .config import Config
+from .config import Config, DesktopConfig
def config_option_exists():
return config_file
-def finalize(config: Config):
+def finalize(config: DesktopConfig):
"""
Copy the executable into the final location
For Android deployment, this is done through buildozer
"""
- generated_exec_path = config.generated_files_path / (config.source_file.stem + EXE_FORMAT)
+ dist_format = EXE_FORMAT
+ if config.mode == DesktopConfig.NuitkaMode.STANDALONE and sys.platform != "darwin":
+ dist_format = ".dist"
+
+ generated_exec_path = config.generated_files_path / (config.source_file.stem + dist_format)
if generated_exec_path.exists() and config.exe_dir:
- if sys.platform == "darwin":
- shutil.copytree(generated_exec_path, config.exe_dir / (config.title + EXE_FORMAT),
+ if sys.platform == "darwin" or config.mode == DesktopConfig.NuitkaMode.STANDALONE:
+ shutil.copytree(generated_exec_path, config.exe_dir / (config.title + dist_format),
dirs_exist_ok=True)
else:
shutil.copy(generated_exec_path, config.exe_dir)
print("[DEPLOY] Executed file created in "
- f"{str(config.exe_dir / (config.source_file.stem + EXE_FORMAT))}")
+ f"{str(config.exe_dir / (config.source_file.stem + dist_format))}")
# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+# enables to use typehints for classes that has not been defined yet or imported
+# used for resolving circular imports
+from __future__ import annotations
import logging
import os
+import shlex
import sys
from pathlib import Path
from typing import List
from . import MAJOR_VERSION, run_command
+from .config import DesktopConfig
class Nuitka:
else:
return "--macos-app-icon"
- def create_executable(self, source_file: Path, extra_args: str, qml_files: List[Path],
- qt_plugins: List[str], excluded_qml_plugins: List[str], icon: str,
- dry_run: bool, permissions: List[str]):
+ def _create_windows_command(self, source_file: Path, command: list):
+ """
+ Special case for Windows where the command length is limited to 8191 characters.
+ """
+
+ # if the platform is windows and the command is more than 8191 characters, the command
+ # will fail with the error message "The command line is too long". To avoid this, we will
+ # we will move the source_file to the intermediate source file called deploy_main.py, and
+ # include the Nuitka options direcly in the main file as mentioned in
+ # https://nuitka.net/user-documentation/user-manual.html#nuitka-project-options
+
+ # convert command into a format recognized by Nuitka when written to the main file
+ # the first item is ignore because it is 'python -m nuitka'
+ nuitka_comment_options = []
+ for command_entry in command[4:]:
+ nuitka_comment_options.append(f"# nuitka-project: {command_entry}")
+ nuitka_comment_options_str = "\n".join(nuitka_comment_options)
+ nuitka_comment_options_str += "\n"
+
+ # read the content of the source file
+ new_source_content = (nuitka_comment_options_str
+ + Path(source_file).read_text(encoding="utf-8"))
+
+ # create and write back the new source content to deploy_main.py
+ new_source_file = source_file.parent / "deploy_main.py"
+ new_source_file.write_text(new_source_content, encoding="utf-8")
+
+ return new_source_file
+
+ def create_executable(self, source_file: Path, extra_args: str, qml_files: list[Path],
+ qt_plugins: list[str], excluded_qml_plugins: list[str], icon: str,
+ dry_run: bool, permissions: list[str],
+ mode: DesktopConfig.NuitkaMode):
qt_plugins = [plugin for plugin in qt_plugins if plugin not in self.qt_plugins_to_ignore]
- extra_args = extra_args.split()
+ extra_args = shlex.split(extra_args)
+
+ # macOS uses the --standalone option by default to create an app bundle
if sys.platform == "darwin":
# create an app bundle
extra_args.extend(["--standalone", "--macos-create-app-bundle"])
for permission in permissions:
extra_args.append(permission_pattern.format(permission=permission))
else:
- extra_args.append("--onefile")
+ extra_args.append(f"--{mode.value}")
qml_args = []
if qml_files:
qt_plugins_str = ",".join(qt_plugins)
command.append(f"--include-qt-plugins={qt_plugins_str}")
+ long_command = False
+ if sys.platform == "win32" and len(" ".join(str(cmd) for cmd in command)) > 7000:
+ logging.info("[DEPLOY] Nuitka command too long for Windows. "
+ "Copying the contents of main Python file to an intermediate "
+ "deploy_main.py file")
+ long_command = True
+ new_source_file = self._create_windows_command(source_file=source_file, command=command)
+ command = self.nuitka + [os.fspath(new_source_file)]
+
command_str, _ = run_command(command=command, dry_run=dry_run)
+
+ # if deploy_main.py exists, delete it after the command is run
+ if long_command:
+ os.remove(source_file.parent / "deploy_main.py")
+
return command_str
"""Parse arguments of a Signal call/Slot decorator (type list)."""
result: Arguments = []
for n, arg in enumerate(call.args):
- par_name = f"a{n+1}"
+ par_name = f"a{n + 1}"
par_type = _parse_pyside_type(arg)
result.append({"name": par_name, "type": par_type})
return result
return
source_files = self.project.python_files + self.project.ui_files
- cmd_prefix = [LUPDATE_CMD] + [p.name for p in source_files]
+ project_dir = self.project.project_file.parent
+ cmd_prefix = [LUPDATE_CMD] + [os.fspath(p.relative_to(project_dir)) for p in source_files]
cmd_prefix.append("-ts")
for ts_file in self.project.ts_files:
if requires_rebuild(source_files, ts_file):
cmd = cmd_prefix
cmd.append(ts_file.name)
- run_command(cmd, cwd=self.project.project_file.parent)
+ run_command(cmd, cwd=project_dir)
if __name__ == "__main__":
def android_deploy():
- if not sys.platform == "linux":
- print("pyside6-android-deploy only works from a Linux host")
+ if sys.platform == "win32":
+ print("pyside6-android-deploy only works from a Unix host and not a Windows host",
+ file=sys.stderr)
else:
android_requirements_file = Path(__file__).parent / "requirements-android.txt"
with open(android_requirements_file, 'r', encoding='UTF-8') as file:
set(pyside_MAJOR_VERSION "6")
set(pyside_MINOR_VERSION "7")
-set(pyside_MICRO_VERSION "2")
+set(pyside_MICRO_VERSION "3")
set(pyside_PRE_RELEASE_VERSION_TYPE "")
set(pyside_PRE_RELEASE_VERSION "")
# Copyright (C) 2023 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+from __future__ import annotations
from .events import (
QAsyncioEventLoopPolicy, QAsyncioEventLoop, QAsyncioHandle, QAsyncioTimerHandle
from .futures import QAsyncioFuture
from .tasks import QAsyncioTask
+from typing import Coroutine, Any
+
import asyncio
-import typing
__all__ = [
"QAsyncioEventLoopPolicy", "QAsyncioEventLoop",
]
-def run(coro: typing.Optional[typing.Coroutine] = None,
- keep_running: bool = True,
- quit_qapp: bool = True, *,
- handle_sigint: bool = False,
- debug: typing.Optional[bool] = None) -> typing.Any:
- """Run the QtAsyncio event loop."""
+def run(coro: Coroutine | None = None,
+ keep_running: bool = True, quit_qapp: bool = True, *, handle_sigint: bool = False,
+ debug: bool | None = None) -> Any:
+ """
+ Run the QtAsyncio event loop.
+
+ If there is no instance of a QCoreApplication, QGuiApplication or
+ QApplication yet, a new instance of QCoreApplication is created.
+
+ :param coro: The coroutine to run. Optional if keep_running is
+ True.
+ :param keep_running: If True, QtAsyncio (the asyncio event loop) will
+ continue running after the coroutine finished, or
+ run "forever" if no coroutine was provided.
+ If False, QtAsyncio will stop after the
+ coroutine finished. A coroutine must be provided if
+ this argument is set to False.
+ :param quit_qapp: If True, the QCoreApplication will quit when
+ QtAsyncio (the asyncio event loop) stops.
+ If False, the QCoreApplication will remain active
+ after QtAsyncio stops, and can continue to be used.
+ :param handle_sigint: If True, the SIGINT signal will be handled by the
+ event loop, causing it to stop.
+ :param debug: If True, the event loop will run in debug mode.
+ If False, the event loop will run in normal mode.
+ If None, the default behavior is used.
+ """
# Event loop policies are expected to be deprecated with Python 3.13, with
# subsequent removal in Python 3.15. At that point, part of the current
ret = asyncio.run(coro, debug=debug)
else:
exc = RuntimeError(
- "QtAsyncio was set to keep running after the coroutine "
+ "QtAsyncio was set not to keep running after the coroutine "
"finished, but no coroutine was provided.")
asyncio.set_event_loop_policy(default_policy)
# Copyright (C) 2023 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+from __future__ import annotations
from PySide6.QtCore import (QCoreApplication, QDateTime, QDeadlineTimer,
QEventLoop, QObject, QTimer, QThread, Slot)
from . import futures
from . import tasks
+from typing import Any, Callable
+
import asyncio
import collections.abc
import concurrent.futures
import signal
import socket
import subprocess
-import typing
import warnings
__all__ = [
the actual callable for the executor into this new event loop.
"""
- def __init__(self, func: typing.Callable, *args: typing.Tuple) -> None:
+ def __init__(self, func: Callable, *args: tuple) -> None:
super().__init__()
self._loop: QEventLoop
self._func = func
self._loop = QEventLoop()
asyncio.events._set_running_loop(self._loop)
- QTimer.singleShot(0, self._loop, lambda: self._cb())
+ # The do() function will always be executed from the new executor
+ # thread and never from outside, so using the overload without the
+ # context argument is sufficient.
+ QTimer.singleShot(0, lambda: self._cb())
self._loop.exec()
if self._exception is not None:
https://discuss.python.org/t/removing-the-asyncio-policy-system-asyncio-set-event-loop-policy-in-python-3-15/37553
"""
def __init__(self,
- application: typing.Optional[QCoreApplication] = None,
quit_qapp: bool = True,
handle_sigint: bool = False) -> None:
super().__init__()
- if application is None:
- if QCoreApplication.instance() is None:
- application = QCoreApplication()
- else:
- application = QCoreApplication.instance()
- self._application: QCoreApplication = application # type: ignore[assignment]
+ self._application = QCoreApplication.instance() or QCoreApplication()
# Configure whether the QCoreApplication at the core of QtAsyncio
# should be shut down when asyncio finishes. A special case where one
# this instance is shut down every time.
self._quit_qapp = quit_qapp
- self._event_loop: typing.Optional[asyncio.AbstractEventLoop] = None
+ self._event_loop: asyncio.AbstractEventLoop | None = None
if handle_sigint:
signal.signal(signal.SIGINT, signal.SIG_DFL)
self._event_loop = QAsyncioEventLoop(self._application, quit_qapp=self._quit_qapp)
return self._event_loop
- def set_event_loop(self, loop: typing.Optional[asyncio.AbstractEventLoop]) -> None:
+ def set_event_loop(self, loop: asyncio.AbstractEventLoop | None) -> None:
self._event_loop = loop
def new_event_loop(self) -> asyncio.AbstractEventLoop:
self._quit_from_outside = False
# A set of all asynchronous generators that are currently running.
- self._asyncgens: typing.Set[collections.abc.AsyncGenerator] = set()
+ self._asyncgens: set[collections.abc.AsyncGenerator] = set()
# Starting with Python 3.11, this must be an instance of
# ThreadPoolExecutor.
# asynchonrous generator raises an exception when closed, and two, if
# an exception is raised during the execution of a task. Currently, the
# default exception handler just prints the exception to the console.
- self._exception_handler: typing.Optional[typing.Callable] = self.default_exception_handler
+ self._exception_handler: Callable | None = self.default_exception_handler
# The task factory, if set with set_task_factory(). Otherwise, a new
# task is created with the QAsyncioTask constructor.
- self._task_factory: typing.Optional[typing.Callable] = None
+ self._task_factory: Callable | None = None
# The future that is currently being awaited with run_until_complete().
- self._future_to_complete: typing.Optional[futures.QAsyncioFuture] = None
+ self._future_to_complete: futures.QAsyncioFuture | None = None
self._debug = bool(os.getenv("PYTHONASYNCIODEBUG", False))
future.get_loop().stop()
def run_until_complete(self,
- future: futures.QAsyncioFuture) -> typing.Any: # type: ignore[override]
+ future: futures.QAsyncioFuture) -> Any: # type: ignore[override]
if self.is_closed():
raise RuntimeError("Event loop is closed")
if self.is_running():
self._asyncgens.clear()
async def shutdown_default_executor(self, # type: ignore[override]
- timeout: typing.Union[int, float, None] = None) -> None:
+ timeout: int | float | None = None) -> None:
shutdown_successful = False
if timeout is not None:
deadline_timer = QDeadlineTimer(int(timeout * 1000))
# Scheduling callbacks
- def _call_soon_impl(self, callback: typing.Callable, *args: typing.Any,
- context: typing.Optional[contextvars.Context] = None,
- is_threadsafe: typing.Optional[bool] = False) -> asyncio.Handle:
+ def _call_soon_impl(self, callback: Callable, *args: Any,
+ context: contextvars.Context | None = None,
+ is_threadsafe: bool | None = False) -> asyncio.Handle:
return self._call_later_impl(0, callback, *args, context=context,
is_threadsafe=is_threadsafe)
- def call_soon(self, callback: typing.Callable, *args: typing.Any,
- context: typing.Optional[contextvars.Context] = None) -> asyncio.Handle:
+ def call_soon(self, callback: Callable, *args: Any,
+ context: contextvars.Context | None = None) -> asyncio.Handle:
return self._call_soon_impl(callback, *args, context=context, is_threadsafe=False)
- def call_soon_threadsafe(self, callback: typing.Callable, *args: typing.Any,
- context:
- typing.Optional[contextvars.Context] = None) -> asyncio.Handle:
+ def call_soon_threadsafe(self, callback: Callable, *args: Any,
+ context: contextvars.Context | None = None) -> asyncio.Handle:
if self.is_closed():
raise RuntimeError("Event loop is closed")
if context is None:
context = contextvars.copy_context()
return self._call_soon_impl(callback, *args, context=context, is_threadsafe=True)
- def _call_later_impl(self, delay: typing.Union[int, float],
- callback: typing.Callable, *args: typing.Any,
- context: typing.Optional[contextvars.Context] = None,
- is_threadsafe: typing.Optional[bool] = False) -> asyncio.TimerHandle:
+ def _call_later_impl(self, delay: int | float, callback: Callable, *args: Any,
+ context: contextvars.Context | None = None,
+ is_threadsafe: bool | None = False) -> asyncio.TimerHandle:
if not isinstance(delay, (int, float)):
raise TypeError("delay must be an int or float")
return self._call_at_impl(self.time() + delay, callback, *args, context=context,
is_threadsafe=is_threadsafe)
- def call_later(self, delay: typing.Union[int, float],
- callback: typing.Callable, *args: typing.Any,
- context: typing.Optional[contextvars.Context] = None) -> asyncio.TimerHandle:
+ def call_later(self, delay: int | float, callback: Callable, *args: Any,
+ context: contextvars.Context | None = None) -> asyncio.TimerHandle:
return self._call_later_impl(delay, callback, *args, context=context, is_threadsafe=False)
- def _call_at_impl(self, when: typing.Union[int, float],
- callback: typing.Callable, *args: typing.Any,
- context: typing.Optional[contextvars.Context] = None,
- is_threadsafe: typing.Optional[bool] = False) -> asyncio.TimerHandle:
+ def _call_at_impl(self, when: int | float, callback: Callable, *args: Any,
+ context: contextvars.Context | None = None,
+ is_threadsafe: bool | None = False) -> asyncio.TimerHandle:
""" All call_at() and call_later() methods map to this method. """
if not isinstance(when, (int, float)):
raise TypeError("when must be an int or float")
return QAsyncioTimerHandle(when, callback, args, self, context, is_threadsafe=is_threadsafe)
- def call_at(self, when: typing.Union[int, float],
- callback: typing.Callable, *args: typing.Any,
- context: typing.Optional[contextvars.Context] = None) -> asyncio.TimerHandle:
+ def call_at(self, when: int | float, callback: Callable, *args: Any,
+ context: contextvars.Context | None = None) -> asyncio.TimerHandle:
return self._call_at_impl(when, callback, *args, context=context, is_threadsafe=False)
def time(self) -> float:
return futures.QAsyncioFuture(loop=self)
def create_task(self, # type: ignore[override]
- coro: typing.Union[collections.abc.Generator, collections.abc.Coroutine],
- *, name: typing.Optional[str] = None,
- context: typing.Optional[contextvars.Context] = None) -> tasks.QAsyncioTask:
+ coro: collections.abc.Generator | collections.abc.Coroutine,
+ *, name: str | None = None,
+ context: contextvars.Context | None = None) -> tasks.QAsyncioTask:
if self._task_factory is None:
task = tasks.QAsyncioTask(coro, loop=self, name=name, context=context)
else:
return task
- def set_task_factory(self, factory: typing.Optional[typing.Callable]) -> None:
+ def set_task_factory(self, factory: Callable | None) -> None:
if factory is not None and not callable(factory):
raise TypeError("The task factory must be a callable or None")
self._task_factory = factory
- def get_task_factory(self) -> typing.Optional[typing.Callable]:
+ def get_task_factory(self) -> Callable | None:
return self._task_factory
# Opening network connections
ssl_handshake_timeout=None,
ssl_shutdown_timeout=None,
happy_eyeballs_delay=None, interleave=None):
- raise NotImplementedError
+ raise NotImplementedError("QAsyncioEventLoop.create_connection() is not implemented yet")
async def create_datagram_endpoint(self, protocol_factory,
local_addr=None, remote_addr=None, *,
family=0, proto=0, flags=0,
reuse_address=None, reuse_port=None,
allow_broadcast=None, sock=None):
- raise NotImplementedError
+ raise NotImplementedError(
+ "QAsyncioEventLoop.create_datagram_endpoint() is not implemented yet")
async def create_unix_connection(
self, protocol_factory, path=None, *,
server_hostname=None,
ssl_handshake_timeout=None,
ssl_shutdown_timeout=None):
- raise NotImplementedError
+ raise NotImplementedError(
+ "QAsyncioEventLoop.create_unix_connection() is not implemented yet")
# Creating network servers
ssl_handshake_timeout=None,
ssl_shutdown_timeout=None,
start_serving=True):
- raise NotImplementedError
+ raise NotImplementedError("QAsyncioEventLoop.create_server() is not implemented yet")
async def create_unix_server(
self, protocol_factory, path=None, *,
ssl_handshake_timeout=None,
ssl_shutdown_timeout=None,
start_serving=True):
- raise NotImplementedError
+ raise NotImplementedError("QAsyncioEventLoop.create_unix_server() is not implemented yet")
async def connect_accepted_socket(
self, protocol_factory, sock,
*, ssl=None,
ssl_handshake_timeout=None,
ssl_shutdown_timeout=None):
- raise NotImplementedError
+ raise NotImplementedError(
+ "QAsyncioEventLoop.connect_accepted_socket() is not implemented yet")
# Transferring files
async def sendfile(self, transport, file, offset=0, count=None,
*, fallback=True):
- raise NotImplementedError
+ raise NotImplementedError("QAsyncioEventLoop.sendfile() is not implemented yet")
# TLS Upgrade
server_hostname=None,
ssl_handshake_timeout=None,
ssl_shutdown_timeout=None):
- raise NotImplementedError
+ raise NotImplementedError("QAsyncioEventLoop.start_tls() is not implemented yet")
# Watching file descriptors
def add_reader(self, fd, callback, *args):
- raise NotImplementedError
+ raise NotImplementedError("QAsyncioEventLoop.add_reader() is not implemented yet")
def remove_reader(self, fd):
- raise NotImplementedError
+ raise NotImplementedError("QAsyncioEventLoop.remove_reader() is not implemented yet")
def add_writer(self, fd, callback, *args):
- raise NotImplementedError
+ raise NotImplementedError("QAsyncioEventLoop.add_writer() is not implemented yet")
def remove_writer(self, fd):
- raise NotImplementedError
+ raise NotImplementedError("QAsyncioEventLoop.remove_writer() is not implemented yet")
# Working with socket objects directly
async def sock_recv(self, sock, nbytes):
- raise NotImplementedError
+ raise NotImplementedError("QAsyncioEventLoop.sock_recv() is not implemented yet")
async def sock_recv_into(self, sock, buf):
- raise NotImplementedError
+ raise NotImplementedError("QAsyncioEventLoop.sock_recv_into() is not implemented yet")
async def sock_recvfrom(self, sock, bufsize):
- raise NotImplementedError
+ raise NotImplementedError("QAsyncioEventLoop.sock_recvfrom() is not implemented yet")
async def sock_recvfrom_into(self, sock, buf, nbytes=0):
- raise NotImplementedError
+ raise NotImplementedError("QAsyncioEventLoop.sock_recvfrom_into() is not implemented yet")
async def sock_sendall(self, sock, data):
- raise NotImplementedError
+ raise NotImplementedError("QAsyncioEventLoop.sock_sendall() is not implemented yet")
async def sock_sendto(self, sock, data, address):
- raise NotImplementedError
+ raise NotImplementedError("QAsyncioEventLoop.sock_sendto() is not implemented yet")
async def sock_connect(self, sock, address):
- raise NotImplementedError
+ raise NotImplementedError("QAsyncioEventLoop.sock_connect() is not implemented yet")
async def sock_accept(self, sock):
- raise NotImplementedError
+ raise NotImplementedError("QAsyncioEventLoop.sock_accept() is not implemented yet")
async def sock_sendfile(self, sock, file, offset=0, count=None, *,
fallback=None):
- raise NotImplementedError
+ raise NotImplementedError("QAsyncioEventLoop.sock_sendfile() is not implemented yet")
# DNS
async def getaddrinfo(self, host, port, *,
family=0, type=0, proto=0, flags=0):
- raise NotImplementedError
+ raise NotImplementedError("QAsyncioEventLoop.getaddrinfo() is not implemented yet")
async def getnameinfo(self, sockaddr, flags=0):
- raise NotImplementedError
+ raise NotImplementedError("QAsyncioEventLoop.getnameinfo() is not implemented yet")
# Working with pipes
async def connect_read_pipe(self, protocol_factory, pipe):
- raise NotImplementedError
+ raise NotImplementedError("QAsyncioEventLoop.connect_read_pipe() is not implemented yet")
async def connect_write_pipe(self, protocol_factory, pipe):
- raise NotImplementedError
+ raise NotImplementedError("QAsyncioEventLoop.connect_write_pipe() is not implemented yet")
# Unix signals
def add_signal_handler(self, sig, callback, *args):
- raise NotImplementedError
+ raise NotImplementedError("QAsyncioEventLoop.add_signal_handler() is not implemented yet")
def remove_signal_handler(self, sig):
- raise NotImplementedError
+ raise NotImplementedError(
+ "QAsyncioEventLoop.remove_signal_handler() is not implemented yet")
# Executing code in thread or process pools
def run_in_executor(self,
- executor: typing.Optional[concurrent.futures.ThreadPoolExecutor],
- func: typing.Callable, *args: typing.Tuple) -> asyncio.futures.Future:
+ executor: concurrent.futures.ThreadPoolExecutor | None,
+ func: Callable, *args: tuple) -> asyncio.futures.Future:
if self.is_closed():
raise RuntimeError("Event loop is closed")
if executor is None:
)
def set_default_executor(self,
- executor: typing.Optional[
- concurrent.futures.ThreadPoolExecutor]) -> None:
+ executor: concurrent.futures.ThreadPoolExecutor | None) -> None:
if not isinstance(executor, concurrent.futures.ThreadPoolExecutor):
raise TypeError("The executor must be a ThreadPoolExecutor")
self._default_executor = executor
# Error Handling API
- def set_exception_handler(self, handler: typing.Optional[typing.Callable]) -> None:
+ def set_exception_handler(self, handler: Callable | None) -> None:
if handler is not None and not callable(handler):
raise TypeError("The handler must be a callable or None")
self._exception_handler = handler
- def get_exception_handler(self) -> typing.Optional[typing.Callable]:
+ def get_exception_handler(self) -> Callable | None:
return self._exception_handler
- def default_exception_handler(self, context: typing.Dict[str, typing.Any]) -> None:
+ def default_exception_handler(self, context: dict[str, Any]) -> None:
# TODO
if context["message"]:
print(context["message"])
- def call_exception_handler(self, context: typing.Dict[str, typing.Any]) -> None:
+ def call_exception_handler(self, context: dict[str, Any]) -> None:
if self._exception_handler is not None:
self._exception_handler(context)
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
**kwargs):
- raise NotImplementedError
+ raise NotImplementedError("QAsyncioEventLoop.subprocess_exec() is not implemented yet")
async def subprocess_shell(self, protocol_factory, cmd, *,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
**kwargs):
- raise NotImplementedError
+ raise NotImplementedError("QAsyncioEventLoop.subprocess_shell() is not implemented yet")
class QAsyncioHandle():
CANCELLED = enum.auto()
DONE = enum.auto()
- def __init__(self, callback: typing.Callable, args: typing.Tuple,
- loop: QAsyncioEventLoop, context: typing.Optional[contextvars.Context],
- is_threadsafe: typing.Optional[bool] = False) -> None:
+ def __init__(self, callback: Callable, args: tuple,
+ loop: QAsyncioEventLoop, context: contextvars.Context | None,
+ is_threadsafe: bool | None = False) -> None:
self._callback = callback
self._args = args
self._loop = loop
def _start(self) -> None:
self._schedule_event(self._timeout, lambda: self._cb())
- def _schedule_event(self, timeout: int, func: typing.Callable) -> None:
+ def _schedule_event(self, timeout: int, func: Callable) -> None:
# Do not schedule events from asyncio when the app is quit from outside
# the event loop, as this would cause events to be enqueued after the
# event loop was destroyed.
class QAsyncioTimerHandle(QAsyncioHandle, asyncio.TimerHandle):
- def __init__(self, when: float, callback: typing.Callable, args: typing.Tuple,
- loop: QAsyncioEventLoop, context: typing.Optional[contextvars.Context],
- is_threadsafe: typing.Optional[bool] = False) -> None:
+ def __init__(self, when: float, callback: Callable, args: tuple,
+ loop: QAsyncioEventLoop, context: contextvars.Context | None,
+ is_threadsafe: bool | None = False) -> None:
QAsyncioHandle.__init__(self, callback, args, loop, context, is_threadsafe)
self._when = when
time = self._loop.time()
+
+ # PYSIDE-2644: Timeouts should be rounded up or down instead of only up
+ # as happens with int(). Otherwise, a timeout of e.g. 0.9 would be
+ # handled as 0, where 1 would be more appropriate.
self._timeout = round(max(self._when - time, 0) * 1000)
QAsyncioHandle._start(self)
# Copyright (C) 2023 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+from __future__ import annotations
from . import events
+from typing import Any, Callable
+
import asyncio
import contextvars
import enum
-import typing
class QAsyncioFuture():
DONE_WITH_RESULT = enum.auto()
DONE_WITH_EXCEPTION = enum.auto()
- def __init__(self, *, loop: typing.Optional["events.QAsyncioEventLoop"] = None,
- context: typing.Optional[contextvars.Context] = None) -> None:
+ def __init__(self, *, loop: "events.QAsyncioEventLoop | None" = None,
+ context: contextvars.Context | None = None) -> None:
self._loop: "events.QAsyncioEventLoop"
if loop is None:
self._loop = asyncio.events.get_event_loop() # type: ignore[assignment]
self._context = context
self._state = QAsyncioFuture.FutureState.PENDING
- self._result: typing.Any = None
- self._exception: typing.Optional[BaseException] = None
+ self._result: Any = None
+ self._exception: BaseException | None = None
- self._cancel_message: typing.Optional[str] = None
+ self._cancel_message: str | None = None
# List of callbacks that are called when the future is done.
- self._callbacks: typing.List[typing.Callable] = list()
+ self._callbacks: list[Callable] = list()
def __await__(self):
if not self.done():
__iter__ = __await__
- def _schedule_callbacks(self, context: typing.Optional[contextvars.Context] = None):
+ def _schedule_callbacks(self, context: contextvars.Context | None = None):
""" A future can optionally have callbacks that are called when the future is done. """
for cb in self._callbacks:
self._loop.call_soon(
cb, self, context=context if context else self._context)
- def result(self) -> typing.Union[typing.Any, Exception]:
+ def result(self) -> Any | Exception:
if self._state == QAsyncioFuture.FutureState.DONE_WITH_RESULT:
return self._result
if self._state == QAsyncioFuture.FutureState.DONE_WITH_EXCEPTION and self._exception:
raise asyncio.CancelledError
raise asyncio.InvalidStateError
- def set_result(self, result: typing.Any) -> None:
+ def set_result(self, result: Any) -> None:
self._result = result
self._state = QAsyncioFuture.FutureState.DONE_WITH_RESULT
self._schedule_callbacks()
def cancelled(self) -> bool:
return self._state == QAsyncioFuture.FutureState.CANCELLED
- def add_done_callback(self, cb: typing.Callable, *,
- context: typing.Optional[contextvars.Context] = None) -> None:
+ def add_done_callback(self, cb: Callable, *,
+ context: contextvars.Context | None = None) -> None:
if self.done():
self._loop.call_soon(
cb, self, context=context if context else self._context)
else:
self._callbacks.append(cb)
- def remove_done_callback(self, cb: typing.Callable) -> int:
+ def remove_done_callback(self, cb: Callable) -> int:
original_len = len(self._callbacks)
self._callbacks = [_cb for _cb in self._callbacks if _cb != cb]
return original_len - len(self._callbacks)
- def cancel(self, msg: typing.Optional[str] = None) -> bool:
+ def cancel(self, msg: str | None = None) -> bool:
if self.done():
return False
self._state = QAsyncioFuture.FutureState.CANCELLED
self._schedule_callbacks()
return True
- def exception(self) -> typing.Optional[BaseException]:
+ def exception(self) -> BaseException | None:
if self._state == QAsyncioFuture.FutureState.CANCELLED:
raise asyncio.CancelledError
if self.done():
# Copyright (C) 2023 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+from __future__ import annotations
from . import events
from . import futures
+from typing import Any
+
import asyncio
import collections.abc
import concurrent.futures
import contextvars
-import typing
class QAsyncioTask(futures.QAsyncioFuture):
""" https://docs.python.org/3/library/asyncio-task.html """
- def __init__(self, coro: typing.Union[collections.abc.Generator, collections.abc.Coroutine], *,
- loop: typing.Optional["events.QAsyncioEventLoop"] = None,
- name: typing.Optional[str] = None,
- context: typing.Optional[contextvars.Context] = None) -> None:
+ def __init__(self, coro: collections.abc.Generator | collections.abc.Coroutine, *,
+ loop: "events.QAsyncioEventLoop | None" = None, name: str | None = None,
+ context: contextvars.Context | None = None) -> None:
super().__init__(loop=loop, context=context)
self._coro = coro # The coroutine for which this task was created.
# The task creates a handle for its coroutine. The handle enqueues the
# task's step function as its callback in the event loop.
- self._handle = self._loop.call_soon(self._step, context=self._context)
+ self._loop.call_soon(self._step, context=self._context)
# The task step function executes the coroutine until it finishes,
# raises an exception or returns a future. If a future was returned,
- # the task will await its completion (or exception).
- self._future_to_await: typing.Optional[asyncio.Future] = None
+ # the task will await its completion (or exception). If the task is
+ # cancelled while it awaits a future, this future must also be
+ # cancelled in order for the cancellation to be successful.
+ self._future_to_await: asyncio.Future | None = None
- self._cancelled = False
- self._cancel_message: typing.Optional[str] = None
+ self._cancelled = False # PYSIDE-2644; see _step
+ self._cancel_count = 0
+ self._cancel_message: str | None = None
# https://docs.python.org/3/library/asyncio-extending.html#task-lifetime-support
asyncio._register_task(self) # type: ignore[arg-type]
class QtTaskApiMisuseError(Exception):
pass
- def set_result(self, result: typing.Any) -> None: # type: ignore[override]
+ def set_result(self, result: Any) -> None: # type: ignore[override]
# This function is not inherited from the Future APIs.
raise QAsyncioTask.QtTaskApiMisuseError("Tasks cannot set results")
- def set_exception(self, exception: typing.Any) -> None: # type: ignore[override]
+ def set_exception(self, exception: Any) -> None: # type: ignore[override]
# This function is not inherited from the Future APIs.
raise QAsyncioTask.QtTaskApiMisuseError("Tasks cannot set exceptions")
def _step(self,
- exception_or_future: typing.Union[
- BaseException, futures.QAsyncioFuture, None] = None) -> None:
+ exception_or_future: BaseException | futures.QAsyncioFuture | None = None) -> None:
"""
The step function is the heart of a task. It is scheduled in the event
loop repeatedly, executing the coroutine "step" by "step" (i.e.,
result = None
self._future_to_await = None
+ if self._cancelled:
+ exception_or_future = asyncio.CancelledError(self._cancel_message)
+ self._cancelled = False
+
if asyncio.futures.isfuture(exception_or_future):
try:
exception_or_future.result()
# called again.
result.add_done_callback(
self._step, context=self._context) # type: ignore[arg-type]
+
+ # The task will await the completion (or exception) of this
+ # future. If the task is cancelled while it awaits a future,
+ # this future must also be cancelled.
self._future_to_await = result
+
if self._cancelled:
- # If the task was cancelled, then a new future should be
- # cancelled as well. Otherwise, in some scenarios like
- # a loop inside the task and with bad timing, if the new
+ # PYSIDE-2644: If the task was cancelled at this step and a
+ # new future was created to be awaited, then it should be
+ # cancelled as well. Otherwise, in some scenarios like a
+ # loop inside the task and with bad timing, if the new
# future is not cancelled, the task would continue running
# in this loop despite having been cancelled. This bad
# timing can occur especially if the first future finishes
asyncio._leave_task(self._loop, self) # type: ignore[arg-type]
if self._exception:
+ message = str(self._exception)
+ if message == "None":
+ message = ""
+ else:
+ message = "An exception occurred during task execution"
self._loop.call_exception_handler({
- "message": (str(self._exception) if self._exception
- else "An exception occurred during task "
- "execution"),
+ "message": message,
"exception": self._exception,
"task": self,
"future": (exception_or_future
# https://docs.python.org/3/library/asyncio-extending.html#task-lifetime-support
asyncio._unregister_task(self) # type: ignore[arg-type]
- def get_stack(self, *, limit=None) -> typing.List[typing.Any]:
+ def get_stack(self, *, limit=None) -> list[Any]:
# TODO
raise NotImplementedError("QtTask.get_stack is not implemented")
# TODO
raise NotImplementedError("QtTask.print_stack is not implemented")
- def get_coro(self) -> typing.Union[collections.abc.Generator, collections.abc.Coroutine]:
+ def get_coro(self) -> collections.abc.Generator | collections.abc.Coroutine:
return self._coro
def get_name(self) -> str:
def set_name(self, value) -> None:
self._name = str(value)
- def cancel(self, msg: typing.Optional[str] = None) -> bool:
+ def cancel(self, msg: str | None = None) -> bool:
if self.done():
return False
+ self._cancel_count += 1
self._cancel_message = msg
- self._handle.cancel()
if self._future_to_await is not None:
# A task that is awaiting a future must also cancel this future in
# order for the cancellation to be successful.
self._future_to_await.cancel(msg)
- self._cancelled = True
+ self._cancelled = True # PYSIDE-2644; see _step
return True
- def uncancel(self) -> None:
- # TODO
- raise NotImplementedError("QtTask.uncancel is not implemented")
+ def uncancel(self) -> int:
+ if self._cancel_count > 0:
+ self._cancel_count -= 1
+ return self._cancel_count
- def cancelling(self) -> bool:
- # TODO
- raise NotImplementedError("QtTask.cancelling is not implemented")
+ def cancelling(self) -> int:
+ return self._cancel_count
<value-type name="QModelRoleData"/>
- <object-type name="QAbstractItemModel">
+ <!-- Register meta type for QML properties -->
+ <object-type name="QAbstractItemModel" qt-register-metatype="yes">
<enum-type name="CheckIndexOption" flags="CheckIndexOptions"/>
<enum-type name="LayoutChangeHint"/>
<!-- This function was replaced by a added function -->
</inject-documentation>
<inject-code class="target" position="beginning" file="../glue/qtcore.cpp" snippet="qobject-findchild-2"/>
- <modify-argument index="return">
+ <modify-argument index="return" pyi-type="Optional[PlaceHolderType]">
<parent index="this" action="add"/>
</modify-argument>
</add-function>
Like the method *findChild*, the first parameter should be the child's type.
</inject-documentation>
<inject-code class="target" position="beginning" file="../glue/qtcore.cpp" snippet="qobject-findchildren"/>
- <modify-argument index="return">
+ <modify-argument index="return" pyi-type="Iterable[PlaceHolderType]">
<parent index="this" action="add"/>
</modify-argument>
</add-function>
<add-function signature="findChildren(PyTypeObject*@type@,const QRegularExpression&@pattern@,Qt::FindChildOptions@options@=Qt::FindChildrenRecursively)"
return-type="PySequence*" >
<inject-code class="target" position="beginning" file="../glue/qtcore.cpp" snippet="qobject-findchildren"/>
- <modify-argument index="return">
+ <modify-argument index="return" pyi-type="Iterable[PlaceHolderType]">
<parent index="this" action="add"/>
</modify-argument>
</add-function>
<add-function signature="__msetitem__">
<inject-code class="target" position="beginning" file="../glue/qtcore.cpp" snippet="qbytearray-msetitem"/>
</add-function>
+ <modify-function signature="fromRawData(const char*, qsizetype)">
+ <modify-argument index="1" pyi-type="str"/>
+ </modify-function>
</value-type>
<primitive-type name="QByteArrayView" view-on="QByteArray">
<conversion-rule>
${Qt${QT_MAJOR_VERSION}Core_PRIVATE_INCLUDE_DIRS}
${Qt${QT_MAJOR_VERSION}Gui_PRIVATE_INCLUDE_DIRS})
+if (${CMAKE_SYSTEM_NAME} STREQUAL "Android")
+ if (QT_FEATURE_opengles2)
+ # add openGL ES 2.0
+ find_package(GLESv2 REQUIRED)
+ else()
+ message(FATAL_ERROR "QtGui requires OpenGL ES 2.0 on Android")
+ endif()
+endif()
+
configure_file("${QtGui_SOURCE_DIR}/QtGui_global.post.h.in"
"${QtGui_BINARY_DIR}/QtGui_global.post.h" @ONLY)
<enum-type name="PlaybackState" since="6.1"/>
<enum-type name="Error"/>
<enum-type name="Loops" python-type="IntEnum" since="6.2.3"/>
+ <modify-function signature="setSource(QUrl)" allow-thread="true"/>
</object-type>
<!-- see qtmultimedia/5773f7214c7430a98dea3974c0597cb3ee0ea7f5 might reappear in 6.3
<object-type name="QMediaPlaylist"/>
<define-ownership class="target" owner="default"/>
</modify-argument>
</modify-function>
- <modify-function signature="get(const QNetworkRequest&)" allow-thread="yes"/>
- <modify-function signature="post(const QNetworkRequest &,QIODevice*)" allow-thread="yes"/>
- <modify-function signature="post(const QNetworkRequest &,const QByteArray &)" allow-thread="yes"/>
- <modify-function signature="put(const QNetworkRequest &,QIODevice*)" allow-thread="yes"/>
- <modify-function signature="put(const QNetworkRequest &,const QByteArray &)" allow-thread="yes"/>
- <modify-function signature="sendCustomRequest(const QNetworkRequest &,const QByteArray &,QIODevice*)" allow-thread="yes" since="4.7"/>
+ <modify-function signature="head(QNetworkRequest)" allow-thread="yes">
+ <modify-argument index="0"> <!-- Suppress return value heuristics -->
+ <define-ownership class="target" owner="default"/>
+ </modify-argument>
+ </modify-function>
+ <modify-function signature="get(QNetworkRequest)" allow-thread="yes">
+ <modify-argument index="0"> <!-- Suppress return value heuristics -->
+ <define-ownership class="target" owner="default"/>
+ </modify-argument>
+ </modify-function>
+ <modify-function signature="get(QNetworkRequest,QIODevice*)" allow-thread="yes">
+ <modify-argument index="0"> <!-- Suppress return value heuristics -->
+ <define-ownership class="target" owner="default"/>
+ </modify-argument>
+ </modify-function>
+ <modify-function signature="get(QNetworkRequest,QByteArray)" allow-thread="yes">
+ <modify-argument index="0"> <!-- Suppress return value heuristics -->
+ <define-ownership class="target" owner="default"/>
+ </modify-argument>
+ </modify-function>
+ <modify-function signature="post(QNetworkRequest,QIODevice*)" allow-thread="yes">
+ <modify-argument index="0"> <!-- Suppress return value heuristics -->
+ <define-ownership class="target" owner="default"/>
+ </modify-argument>
+ </modify-function>
+ <modify-function signature="post(QNetworkRequest,QByteArray)" allow-thread="yes">
+ <modify-argument index="0"> <!-- Suppress return value heuristics -->
+ <define-ownership class="target" owner="default"/>
+ </modify-argument>
+ </modify-function>
+ <modify-function signature="put(QNetworkRequest,QIODevice*)" allow-thread="yes">
+ <modify-argument index="0"> <!-- Suppress return value heuristics -->
+ <define-ownership class="target" owner="default"/>
+ </modify-argument>
+ </modify-function>
+ <modify-function signature="put(QNetworkRequest,QByteArray)" allow-thread="yes">
+ <modify-argument index="0"> <!-- Suppress return value heuristics -->
+ <define-ownership class="target" owner="default"/>
+ </modify-argument>
+ </modify-function>
+ <modify-function signature="sendCustomRequest(QNetworkRequest,QByteArray,QIODevice*)" allow-thread="yes">
+ <modify-argument index="0"> <!-- Suppress return value heuristics -->
+ <define-ownership class="target" owner="default"/>
+ </modify-argument>
+ </modify-function>
+ <modify-function signature="sendCustomRequest(QNetworkRequest,QByteArray,QByteArray)" allow-thread="yes">
+ <modify-argument index="0"> <!-- Suppress return value heuristics -->
+ <define-ownership class="target" owner="default"/>
+ </modify-argument>
+ </modify-function>
<modify-function signature="setCache(QAbstractNetworkCache*)">
<modify-argument index="1">
<define-ownership class="target" owner="c++"/>
<value-type name="MultiplexValueRange"/>
</value-type>
<value-type name="QCanUniqueIdDescription"/>
- <object-type name="QModbusClient"/>
+ <object-type name="QModbusClient">
+ <modify-function signature="sendReadRequest(QModbusDataUnit,int)">
+ <modify-argument index="0"> <!-- Suppress return value heuristics -->
+ <define-ownership class="target" owner="default"/>
+ </modify-argument>
+ </modify-function>
+ <modify-function signature="sendWriteRequest(QModbusDataUnit,int)">
+ <modify-argument index="0"> <!-- Suppress return value heuristics -->
+ <define-ownership class="target" owner="default"/>
+ </modify-argument>
+ </modify-function>
+ <modify-function signature="sendReadWriteRequest(QModbusDataUnit,QModbusDataUnit,int)">
+ <modify-argument index="0"> <!-- Suppress return value heuristics -->
+ <define-ownership class="target" owner="default"/>
+ </modify-argument>
+ </modify-function>
+ <modify-function signature="sendRawRequest(QModbusRequest,int)">
+ <modify-argument index="0"> <!-- Suppress return value heuristics -->
+ <define-ownership class="target" owner="default"/>
+ </modify-argument>
+ </modify-function>
+ </object-type>
<value-type name="QModbusDataUnit">
<enum-type name="RegisterType"/>
</value-type>
</modify-argument>
</modify-function>
+ <modify-function signature="postEvent(QEvent*,QStateMachine::EventPriority)">
+ <modify-argument index="1">
+ <define-ownership owner="c++"/>
+ </modify-argument>
+ </modify-function>
+ <modify-function signature="postDelayedEvent(QEvent*,int)">
+ <modify-argument index="1">
+ <define-ownership owner="c++"/>
+ </modify-argument>
+ </modify-function>
+
<add-function signature="configuration()" return-type="QSet<QAbstractState*>">
<inject-code class="target" position="beginning" file="../glue/qtstatemachine.cpp"
snippet="qstatemachine-configuration"/>
--- /dev/null
+# Copyright (C) 2024 The Qt Company Ltd.
+# SPDX-License-Identifier: BSD-3-Clause
+
+project(QtWebView)
+
+set(QtWebView_Src
+${QtWebView_GEN_DIR}/qtwebview_wrapper.cpp
+# module is always needed
+${QtWebView_GEN_DIR}/qtwebview_module_wrapper.cpp
+)
+
+set(QtWebView_include_dirs ${QtWebView_SOURCE_DIR}
+ ${QtWebView_BINARY_DIR}
+ ${Qt${QT_MAJOR_VERSION}Core_INCLUDE_DIRS}
+ ${Qt${QT_MAJOR_VERSION}Gui_INCLUDE_DIRS}
+ ${Qt${QT_MAJOR_VERSION}WebView_INCLUDE_DIRS}
+ ${libpyside_SOURCE_DIR}
+ ${QtGui_GEN_DIR}
+ ${QtCore_GEN_DIR}
+ ${QtWebView_GEN_DIR})
+
+set(QtWebView_libraries pyside6
+ ${Qt${QT_MAJOR_VERSION}WebView_LIBRARIES})
+
+set(QtWebView_deps QtGui)
+
+# for Windows and Linux, QtWebView depends on QtWebEngine to render content
+if ((WIN32 OR UNIX) AND NOT APPLE)
+ list(APPEND QtWebView_deps QtWebEngineCore QtWebEngineQuick)
+endif()
+
+create_pyside_module(NAME QtWebView
+ INCLUDE_DIRS QtWebView_include_dirs
+ LIBRARIES QtWebView_libraries
+ DEPS QtWebView_deps
+ TYPESYSTEM_PATH QtWebView_SOURCE_DIR
+ SOURCES QtWebView_Src)
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+// Copyright (C) 2024 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+-->
+<typesystem package="PySide6.QtWebView"
+ namespace-begin="QT_BEGIN_NAMESPACE" namespace-end="QT_END_NAMESPACE">
+ <load-typesystem name="QtCore/typesystem_core.xml" generate ="no"/>
+ <namespace-type name="QtWebView"/>
+</typesystem>
</value-type>
<modify-function signature="getLayoutPosition(QLayout*,int*,QFormLayout::ItemRole*)const">
- <modify-argument index="0">
- <replace-type modified-type="PyObject"/>
- </modify-argument>
<modify-argument index="2">
<remove-argument/>
</modify-argument>
<modify-argument index="3">
<remove-argument/>
</modify-argument>
+ <modify-argument index="return" pyi-type="Tuple[int, PySide6.QtWidgets.QFormLayout.ItemRole]">
+ <replace-type modified-type="PyTuple"/>
+ </modify-argument>
<inject-code class="target" position="beginning" file="../glue/qtwidgets.cpp" snippet="qformlayout-fix-args" />
</modify-function>
<modify-function signature="getWidgetPosition(QWidget*,int*,QFormLayout::ItemRole*)const">
from textwrap import dedent
# __all__ is also corrected below.
-__all__ = list("Qt" + body for body in
- "@all_module_shortnames@"
- .split(";"))
+__all__ = [@init_modules@]
__version__ = "@FINAL_PACKAGE_VERSION@"
__version_info__ = (@BINDING_API_MAJOR_VERSION@, @BINDING_API_MINOR_VERSION@, @BINDING_API_MICRO_VERSION@, "@BINDING_API_PRE_RELEASE_VERSION_TYPE@", "@BINDING_API_PRE_RELEASE_VERSION@")
bool arg_qpermission = (classMethod && (count == 2)) || (!classMethod && (count == 1));
-auto callback = [callable, count, arg_qpermission](const QPermission &permission) -> void
+auto callback = [callable, arg_qpermission](const QPermission &permission) -> void
{
Shiboken::GilState state;
if (arg_qpermission) {
list(APPEND shiboken_include_dir_list ${${module_ADDITIONAL_INCLUDE_DIRS}})
endif()
- # Transform the path separators into something shiboken understands.
+ # Remove duplicates and transform the path separators into something
+ # shiboken understands.
+ list(REMOVE_DUPLICATES shiboken_include_dir_list)
make_path(shiboken_include_dirs ${shiboken_include_dir_list})
set(force_process_system_include_paths_list "")
--lean-headers
--api-version=${SUPPORTED_QT_VERSION})
+ # check if building for Android with a macOS host
+ # This is not needed for Linux because OpenGLES2 development binaries in
+ # linux can be installed by installing 'libgles2-mesa-dev' package which
+ # comes as a default requirement for building PySide6. As such for
+ # cross-compiling in linux, we use the clang compiler from the installed
+ # libclang itself.
+ if(CMAKE_ANDROID_ARCH_LLVM_TRIPLE AND CMAKE_HOST_APPLE)
+ message(STATUS "Building for Android with arch ${CMAKE_ANDROID_ARCH_LLVM_TRIPLE}")
+ list(APPEND shiboken_command "--clang-option=--target=${CMAKE_ANDROID_ARCH_LLVM_TRIPLE}")
+
+ # CMAKE_CXX_ANDROID_TOOLCHAIN_PREFIX does not contain the ANDROID_PLATFORM i.e. it ends with
+ # the form 'aarch64-linux-android-'. Remove the last '-' and add the corresponding clang
+ # based on ANDROID_PLATFORM making it 'aarch64-linux-android26-clang++'
+
+ # Get the length of the string
+ string(LENGTH "${CMAKE_CXX_ANDROID_TOOLCHAIN_PREFIX}" _length)
+
+ # Subtract 1 from the length to get the characters till '-'
+ math(EXPR _last_index "${_length} - 1")
+
+ # Get the substring from the start to the character before the last one
+ string(SUBSTRING "${CMAKE_CXX_ANDROID_TOOLCHAIN_PREFIX}" 0 "${_last_index}"
+ SHIBOKEN_ANDROID_COMPILER_PREFIX)
+
+ # use the compiler from the Android NDK
+ list(APPEND shiboken_command
+ "--compiler-path=${SHIBOKEN_ANDROID_COMPILER_PREFIX}${CMAKE_ANDROID_API}-clang++")
+ endif()
+
if(CMAKE_HOST_APPLE)
set(shiboken_framework_include_dir_list ${QT_FRAMEWORK_INCLUDE_DIR})
make_path(shiboken_framework_include_dirs ${shiboken_framework_include_dir_list})
${generate_pyi_options})
add_dependencies("${module_NAME}_pyi" ${module_NAME})
+ if(TARGET shibokenmodule)
+ add_dependencies("${module_NAME}_pyi" shibokenmodule)
+ endif()
+
install(FILES "${CMAKE_CURRENT_BINARY_DIR}/../${module_NAME}.pyi"
DESTINATION "${PYTHON_SITE_PACKAGES}/PySide6")
endif()
endif()
list(APPEND ALL_OPTIONAL_MODULES WebChannel WebEngineCore WebEngineWidgets
WebEngineQuick WebSockets HttpServer)
+ # for Windows and Linux, QtWebView depends on QtWebEngine to render content
+ if ((WIN32 OR UNIX) AND NOT APPLE AND Qt${QT_MAJOR_VERSION}WebEngineCore_FOUND AND
+ Qt${QT_MAJOR_VERSION}WebEngineQuick_FOUND)
+ list(APPEND ALL_OPTIONAL_MODULES WebView)
+ endif()
list(APPEND ALL_OPTIONAL_MODULES 3DCore 3DRender 3DInput 3DLogic 3DAnimation 3DExtras)
endmacro()
set(ENABLE_MAC "0")
set(ENABLE_WIN "0")
- if(CMAKE_HOST_APPLE)
- set(ENABLE_MAC "1")
- elseif(CMAKE_HOST_WIN32)
- set(ENABLE_WIN "1")
- set(ENABLE_UNIX "0")
- elseif(NOT CMAKE_HOST_UNIX)
- message(FATAL_ERROR "OS not supported")
+ # check if Android, if so, set ENABLE_UNIX=1
+ # this is needed to avoid including the wrapper specific to macOS when building for Android
+ # from a macOS host
+ if(NOT CMAKE_SYSTEM_NAME STREQUAL "Android")
+ if(CMAKE_HOST_APPLE)
+ set(ENABLE_MAC "1")
+ elseif(CMAKE_HOST_WIN32)
+ set(ENABLE_WIN "1")
+ set(ENABLE_UNIX "0")
+ elseif(NOT CMAKE_HOST_UNIX)
+ message(FATAL_ERROR "OS not supported")
+ endif()
endif()
endmacro()
list(REMOVE_ITEM DISABLED_MODULES ${m})
endif()
endforeach()
+
+ # Special list of modules for the __init__.py.in file
+ foreach(im ${all_module_shortnames})
+ list(APPEND init_modules "\"Qt${im}\"")
+ endforeach()
+ string(REPLACE ";" ", " init_modules "${init_modules}")
endmacro()
macro(collect_module_if_found shortname)
# If the module was found, and also the module path is the same as the
# Qt5Core base path, we will generate the list with the modules to be installed
set(looked_in_message ". Looked in: ${${_name_dir}}")
- if("${${_name_found}}" AND (("${found_basepath}" GREATER "0") OR ("${found_basepath}" EQUAL "0")))
+
+ # 'found_basepath' is used to ensure consistency that all the modules are from the same Qt
+ # directory which prevents issues from arising due to mixing versions or using incompatible Qt
+ # modules. When SHIBOKEN_FORCE_PROCESS_SYSTEM_INCLUDE_PATHS is not empty, we can ignore this
+ # requirement of 'found_basepath'.
+ # This is specifically useful for Flatpak build of PySide6 where For Flatpak the modules are in
+ # different directories. For Flatpak, although the modules are in different directories, they
+ # are all compatible.
+ if("${${_name_found}}" AND
+ ((("${found_basepath}" GREATER "0") OR ("${found_basepath}" EQUAL "0")) OR
+ (NOT SHIBOKEN_FORCE_PROCESS_SYSTEM_INCLUDE_PATHS STREQUAL "")))
message(STATUS "${module_state} module ${name} found (${ARGN})${looked_in_message}")
# record the shortnames for the tests
list(APPEND all_module_shortnames ${shortname})
to run the coroutine and then stop the event loop upon its completion.
This latter case behaves identically to ``asyncio.run(my_coroutine())``.
+If there is no instance of a QCoreApplication, QGuiApplication or
+QApplication yet, a new instance of QCoreApplication is created.
+
An additional optional argument ``quit_qapp`` can be passed to ``run()``
to configure whether the QCoreApplication at the core of QtAsyncio
should be shut down when asyncio finishes. A special case where one
Coroutines are functions that can be paused (yield) and resumed. Behind
this simple concept lies a complex mechanism that is abstracted by the
-asynchronous framework. This talk presents a diagram that attempts to
-illustrate the flow of a coroutine from the moment it's provided to the
-async framework until it's completed.
+asynchronous framework. `This talk <https://www.youtube.com/watch?v=XuqdTvisqkQ>`_
+presents the below diagram that attempts to illustrate the flow of a
+coroutine from the moment it's provided to the async framework until
+it's completed.
-.. image:: https://img.youtube.com/vi/XuqdTvisqkQ/mqdefault.jpg
- :alt: Asynchronous programming with asyncio and Qt
- :target: https://www.youtube.com/watch?v=XuqdTvisqkQ
+.. image:: coroutines.png
+ :alt: Coroutines explained
Complementary to the wheels, you will be able to download the sources
as well.
-.. note:: Wheels installed this way will be detectable by `*Qt Creator*`_, which
+.. note:: Wheels installed this way will be detectable by `Qt Creator`_, which
will offer you to install them for your current Python interpreter.
Using account.qt.io
This process takes a bit longer, but in the end you have one executable ``hello.bin``::
./hello.bin
-
-
-Some Caveats
-============
-
-
-Nuitka issue on macOS
----------------------
-
-Nuitka currently has a problem with the macOS bundle files on current macOS versions.
-That has the effect that ``--standalone`` and ``--onefile`` create a crashing application.
-Older versions which don't have the recent macOS API changes from 2020 will work.
-We are currently trying to fix that problem.
The final executable produced has a ``.exe`` suffix on Windows, ``.bin`` on Linux and ``.app`` on
macOS.
-.. note:: Although using a virtual environment for Python is recommended for ``pyside6-deploy``, do
- not add the virtual environment to the application directory you are trying to deploy.
- ``pyside6-deploy`` will try to package this venv folder and will eventually fail.
-
-.. note:: The default version of Nuitka used with the tool is version ``2.3.2``. This can be
+.. note:: The default version of Nuitka used with the tool is version ``2.4.8``. This can be
updated to a newer version by updating your ``pysidedeploy.spec`` file.
.. _how_pysidedeploy:
NSCameraUsageDescription:CameraAccess
+ * ``mode``: Accepts one of the options: ``onefile`` or ``standalone``. The default is ``onefile``.
+ This option corresponds to the mode in which Nuitka is run. The onefile mode creates a single
+ executable file, while the standalone mode creates a directory with the executable and all the
+ necessary files. The standalone mode is useful when you want to distribute the application as a
+ directory with dependencies and other files required by the app.
+
* ``extra_args``: Any extra Nuitka arguments specified. It is specified as space-separated
command line arguments i.e. just like how you would specify it when you use Nuitka through
the command line. By default, it contains the following arguments::
* - macOS
- dyld_info
- Available by default from macOS 12 and upwards
+
+Creating a bug report
+=====================
+
+If you are unsure if the bug is from ``pyside6-deploy`` or ``Nuitka``:
+
+#. Create a bug report in Qt for Python. See instructions
+ `here <https://wiki.qt.io/Qt_for_Python/Reporting_Bugs/>`_.
+
+#. Run ``pyside6-deploy`` command with the ``--verbose`` option and replace ``--quiet`` with
+ ``--verbose`` in the ``extra_args`` parameter in the ``pysidedeploy.spec`` file. Attach the
+ output from stdout to the bug report.
+
+#. Attach a minimal example that reproduces the bug with the bug report.
+
+If you think the bug originates from ``Nuitka``:
+
+#. Try using a newer version of ``Nuitka``. You can change this from the ``packages`` parameter in
+ your generated ``pysidedeploy.spec`` file.
+
+#. If the bug persists, create a bug report on the
+ `Nuitka GitHub page <https://github.com/Nuitka/Nuitka/issues>`_.
+
+ * Run ``pyside6-deploy`` with the ``--dry-run`` option to see the actual ``Nuitka`` command
+ generated. Attach the ``Nuitka`` command ran to the bug report.
+ * Follow the Nuitka bug report template to create a bug report.
documentation.rst
adapt_qt.rst
extras.rst
+ qtasyncio.rst
Implementation details
----------------------
--- /dev/null
+.. _developer-qtasyncio:
+
+QtAsyncio developer notes
+=========================
+
+QtAsyncio is a module that provides integration between Qt and Python's
+`asyncio`_ module. It allows you to run `asyncio` event loops in a Qt
+application, and to use Qt APIs from `asyncio` coroutines.
+
+.. _asyncio: https://docs.python.org/3/library/asyncio.html
+
+As a pure Python module that integrates deeply with another Python
+module, developing for QtAsyncio has some interesting aspects that
+differ from other parts of Qt for Python.
+
+QtAsyncio vs asyncio
+--------------------
+
+QtAsyncio has an interesting property from a developer perspective. As
+one of its purposes is to be a transparent replacement to asyncio's own
+event loop, for any given code that uses asyncio APIs, QtAsyncio will
+(or should!) behave identically to asyncio. (Note that the reverse does
+not apply if we also have Qt code). This is especially handy when
+debugging, as one can run the asyncio code to compare and to check the
+expected behavior. It also helps to debug both QtAsyncio and asyncio
+step by step and compare the program flow to find where QtAsyncio might
+go wrong (as far as their code differences allow). Often, one will also
+be able to implement a unit test by asserting that QtAsyncio's output is
+identical to asyncio's. (Note that a limitation to this is the fact that
+Qt's event loop does not guarantee the order of execution of callbacks,
+while asyncio does.)
+
+The step function
+-----------------
+
+When debugging, a significant amount of time will likely be spent inside
+the ``_step`` method of the ``QAsyncioTask`` class, as it is called
+often and it is where coroutines are executed. You will find that it is
+similar to asyncio's equivalent method, but with some differences. Read
+the in-line comments carefully. Some variables and associated lines of
+code might seem innocuous or even unnecessary, but provide important bug
+fixes.
+
+Keeping QtAsyncio in sync with asyncio
+--------------------------------------
+
+QtAsyncio implements `asyncio's API <https://docs.python.org/3/library/asyncio.html>`_,
+which can change with new releases. As it is part of the standard
+library, this lines up with new Python versions. It is therefore good
+practice to check whether the API has changed for every new Python
+version. This can include new classes or methods, changed signatures for
+existing functions, deprecations or outright removals. For example,
+event loop policies are expected to be deprecated with Python 3.13, with
+subsequent removal in Python 3.15. This removal will in turn require a
+small refactoring to make sure the ``QtAsyncio.run()`` function no
+longer uses the policy mechanism.
+
+asyncio developer guidance
+--------------------------
+
+asyncio's resources go beyond the pure API documentation. E.g., asyncio
+provides its own `developer guidance <https://docs.python.org/3/library/asyncio-dev.html>`_
+that is worth following when developing for QtAsyncio. In addition,
+there is documentation for
+`extending asyncio <https://docs.python.org/3/library/asyncio-extending.html>`_
+that details a few essential points to keep in mind when building your
+own event loop.
--- /dev/null
+
+Qt WebView lets you display web content inside a QML application. To avoid including a full web
+browser stack, Qt WebView uses native APIs where appropriate.
+
+Getting Started
+^^^^^^^^^^^^^^^
+
+To include the definitions of modules classes, use the following
+directive:
+
+ ::
+
+ from PySide6.QtWebView import QtWebView
+
+To make the Qt WebView module function correctly across all platforms, it's
+necessary to call ``QtWebView.initialize()`` before creating the QGuiApplication
+instance and before window's QPlatformOpenGLContext is created. For usage,
+see the ``minibrowser`` example in the PySide6 examples package.
+
+API Reference
+^^^^^^^^^^^^^
+
+ * `Qt API <https://doc.qt.io/qt-6/qtwebview-index.html>`_
+
+The module also provides `QML types <https://doc.qt.io/qt-6/qtwebview-index.html#qml-api>`_
Provides WebSocket communication compliant with RFC 6455.
+ .. grid-item-card:: :mod:`QtWebView <PySide6.QtWebView>`
+
+ Enables displaying web content in a QML application.
+
.. grid-item-card:: :mod:`QtWidgets <PySide6.QtWidgets>`
Extends Qt GUI with C++ widget functionality.
.. grid-item-card:: :mod:`QtAsyncio <PySide6.QtAsyncio>`
Provides integration between asyncio and Qt's event loop.
+
* **Declarative UI**
- The UI can be described in the QML language (assigned to a Python variable)::
+ The UI can be described in the QML language:
+
+ .. code-block:: javascript
- QML = """
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
}
}
}
- """
- .. note:: Keep in mind ideally this content should go into
- a ``qml`` file, but for simplicity, we are using a string variable.
+ Put the this into a file named :code:`Main.qml` into a directory named
+ :code:`Main` along with a file named :code:`qmldir` to describe a basic
+ QML module:
+
+ .. code-block:: text
+
+ module Main
+ Main 254.0 Main.qml
* **Application execution**
Now, add a main function where you instantiate a :ref:`QQmlApplicationEngine` and
load the QML::
+ import sys
+ from PySide6.QtGui import QGuiApplication
+ from PySide6.QtQml import QQmlApplicationEngine
+
if __name__ == "__main__":
app = QGuiApplication(sys.argv)
engine = QQmlApplicationEngine()
- engine.loadData(QML.encode('utf-8'))
+ engine.addImportPath(sys.path[0])
+ engine.loadFromModule("Main", "Main")
if not engine.rootObjects():
sys.exit(-1)
exit_code = app.exec()
del engine
sys.exit(exit_code)
-
- .. note:: This is a simplified example. Normally, the QML code should be in a separate
- :code:`.qml` file, which can be edited by design tools.
-
.. _faq-section:
Frequently Asked Questions
pyside6-qml
===========
-``pyside6-qml`` mimics some capabilities of Qt's `qml <qml_runtime>`_ runtime utility by directly
+``pyside6-qml`` mimics some capabilities of Qt's `qml`_ runtime utility by directly
invoking QQmlEngine/QQuickView. It enables prototyping with QML/QtQuick without the need to write
-any Python code that loads the QML files either through `QQmlApplicationEngine <qqmlappengine>`_ or
-the `QQuickView <qquickview>`_ class. The tool also detects the QML classes implemented in Python
+any Python code that loads the QML files either through `QQmlApplicationEngine`_ or
+the `QQuickView`_ class. The tool also detects the QML classes implemented in Python
and registers them with the QML type system.
Usage
-----
-Consider the example `Extending QML - Plugins Example <extending_qml_example>`_. This example does
+Consider the example `Extending QML - Plugins Example`_. This example does
not have a Python file with a ``main`` function that initializes a QmlEngine to load the QML file
``app.qml``. You can run the example by running
* **--verbose/-v**: Run ``pyside6-qml`` in verbose mode. When run in this mode, pyside6-qml prints
log messages during various stages of processing.
-Options that align with `QML <qml_runtime>`_ runtime utility
-^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+Options that align with the `qml`_ runtime utility
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
* **--app-typ/-a**: Specifies which application class to use. It takes one of the three values -
``core, gui, widget``. The default value is *gui*.
* **--disable-context-sharing**: Disable the use of a shared GL context for QtQuick Windows".
-.. _`qml_runtime`: https://doc.qt.io/qt-6/qtquick-qml-runtime.html
-.. _`qqmlappengine`: https://doc.qt.io/qt-6/qqmlapplicationengine.html
-.. _`qquickview`: https://doc.qt.io/qt-6/qquickview.html
-.. _`extending_qml_example`: https://doc.qt.io/qtforpython-6/examples/example_qml_tutorials_extending-qml_chapter6-plugins.html
+.. _`qml`: https://doc.qt.io/qt-6/qtquick-qml-runtime.html
+.. _`QQmlApplicationEngine`: https://doc.qt.io/qt-6/qqmlapplicationengine.html
+.. _`QQuickView`: https://doc.qt.io/qt-6/qquickview.html
+.. _`Extending QML - Plugins Example`: https://doc.qt.io/qtforpython-6/examples/example_qml_tutorials_extending-qml_chapter6-plugins.html
this tutorial, we will show how to make a simple "Hello World"
application with PySide6 and QML.
-A PySide6/QML application consists, at least, of two different files -
+A PySide6/QML application consists, mainly, of two different files -
a file with the QML description of the user interface, and a python file
-that loads the QML file. To make things easier, let's save both files in
-the same directory.
+that loads the QML file.
-Here is a simple QML file called :code:`view.qml`:
+Here is a simple QML file called :code:`Main.qml`:
.. code-block:: javascript
the text appear centered within the object with :code:`id: main`,
which is the Rectangle in this case.
+Put the file into into a directory named :code:`Main` along
+with a file named :code:`qmldir` to describe a basic QML module:
+
+.. code-block:: text
+
+ module Main
+ Main 254.0 Main.qml
+
Now, let's see how the code looks on the PySide6.
Let's call it :code:`main.py`:
.. code-block:: python
import sys
- from PySide6.QtWidgets import QApplication
+ from PySide6.QtGui import QGuiApplication
from PySide6.QtQuick import QQuickView
if __name__ == "__main__":
- app = QApplication()
+ app = QGuiApplication()
view = QQuickView()
-
- view.setSource("view.qml")
+ view.engine().addImportPath(sys.path[0])
+ view.loadFromModule("Main", "Main")
view.show()
- sys.exit(app.exec())
+ ex = app.exec()
+ del view
+ sys.exit(ex)
If you are already familiar with PySide6 and have followed our
tutorials, you have already seen much of this code.
-The only novelties are that you must :code:`import QtQuick` and set the
-source of the :code:`QQuickView` object to the URL of your QML file.
+The only novelties are that you must :code:`import QtQuick`,
+add the directory to the import paths, and instruct the
+:code:`QQuickView` to load our module.
Then, similar to what you do with any Qt widget, you call
:code:`QQuickView.show()`.
Changes in the code
===================
-As you are modifying an existing example, you need to modify the following
-lines:
+As you are modifying an existing example, you need to modify the
+``player.py`` file. At the top, change
.. code-block:: python
from PySide6.QtGui import QIcon, QKeySequence
- playIcon = self.style().standardIcon(QStyle.SP_MediaPlay)
- previousIcon = self.style().standardIcon(QStyle.SP_MediaSkipBackward)
- pauseIcon = self.style().standardIcon(QStyle.SP_MediaPause)
- nextIcon = self.style().standardIcon(QStyle.SP_MediaSkipForward)
- stopIcon = self.style().standardIcon(QStyle.SP_MediaStop)
-and replace them with the following:
+to:
.. code-block:: python
from PySide6.QtGui import QIcon, QKeySequence, QPixmap
- playIcon = QIcon(QPixmap(":/icons/play.png"))
- previousIcon = QIcon(QPixmap(":/icons/previous.png"))
- pauseIcon = QIcon(QPixmap(":/icons/pause.png"))
- nextIcon = QIcon(QPixmap(":/icons/forward.png"))
- stopIcon = QIcon(QPixmap(":/icons/stop.png"))
-
-This ensures that the new icons are used instead of the default ones provided
-by the application theme.
-Notice that the lines are not consecutive, but are in different parts
-of the file.
-After all your imports, add the following
+Below the imports, add the following:
.. code-block:: python
import rc_icons
-Now, the constructor of your class should look like this:
+In the ``MainWindow.__init__()`` function, replace the code
+loading the icons from the theme by your custom icons:
.. code-block:: python
- def __init__(self):
- super(MainWindow, self).__init__()
-
- self.playlist = QMediaPlaylist()
- self.player = QMediaPlayer()
+ playIcon = QIcon(QPixmap(":/icons/play.png"))
+ self._play_action = tool_bar.addAction(playIcon, "Play")
- toolBar = QToolBar()
- self.addToolBar(toolBar)
+When doing this for all icons, the constructor of your class should
+look like this:
- fileMenu = self.menuBar().addMenu("&File")
- openAction = QAction(QIcon.fromTheme("document-open"),
- "&Open...", self, shortcut=QKeySequence.Open,
- triggered=self.open)
- fileMenu.addAction(openAction)
- exitAction = QAction(QIcon.fromTheme("application-exit"), "E&xit",
- self, shortcut="Ctrl+Q", triggered=self.close)
- fileMenu.addAction(exitAction)
+.. code-block:: python
- playMenu = self.menuBar().addMenu("&Play")
+ def __init__(self):
+ super().__init__()
+
+ self._playlist = []
+ self._playlist_index = -1
+ self._audio_output = QAudioOutput()
+ self._player = QMediaPlayer()
+ self._player.setAudioOutput(self._audio_output)
+
+ self._player.errorOccurred.connect(self._player_error)
+
+ tool_bar = QToolBar()
+ self.addToolBar(tool_bar)
+
+ file_menu = self.menuBar().addMenu("&File")
+ icon = QIcon.fromTheme(QIcon.ThemeIcon.DocumentOpen)
+ open_action = QAction(icon, "&Open...", self,
+ shortcut=QKeySequence.Open, triggered=self.open)
+ file_menu.addAction(open_action)
+ tool_bar.addAction(open_action)
+ icon = QIcon.fromTheme(QIcon.ThemeIcon.ApplicationExit)
+ exit_action = QAction(icon, "E&xit", self,
+ shortcut="Ctrl+Q", triggered=self.close)
+ file_menu.addAction(exit_action)
+
+ play_menu = self.menuBar().addMenu("&Play")
playIcon = QIcon(QPixmap(":/icons/play.png"))
- self.playAction = toolBar.addAction(playIcon, "Play")
- self.playAction.triggered.connect(self.player.play)
- playMenu.addAction(self.playAction)
+ self._play_action = tool_bar.addAction(playIcon, "Play")
+ self._play_action.triggered.connect(self._player.play)
+ play_menu.addAction(self._play_action)
previousIcon = QIcon(QPixmap(":/icons/previous.png"))
- self.previousAction = toolBar.addAction(previousIcon, "Previous")
- self.previousAction.triggered.connect(self.previousClicked)
- playMenu.addAction(self.previousAction)
+ self._previous_action = tool_bar.addAction(previousIcon, "Previous")
+ self._previous_action.triggered.connect(self.previous_clicked)
+ play_menu.addAction(self._previous_action)
pauseIcon = QIcon(QPixmap(":/icons/pause.png"))
- self.pauseAction = toolBar.addAction(pauseIcon, "Pause")
- self.pauseAction.triggered.connect(self.player.pause)
- playMenu.addAction(self.pauseAction)
+ self._pause_action = tool_bar.addAction(pauseIcon, "Pause")
+ self._pause_action.triggered.connect(self._player.pause)
+ play_menu.addAction(self._pause_action)
nextIcon = QIcon(QPixmap(":/icons/forward.png"))
- self.nextAction = toolBar.addAction(nextIcon, "Next")
- self.nextAction.triggered.connect(self.playlist.next)
- playMenu.addAction(self.nextAction)
+ self._next_action = tool_bar.addAction(nextIcon, "Next")
+ self._next_action.triggered.connect(self.next_clicked)
+ play_menu.addAction(self._next_action)
stopIcon = QIcon(QPixmap(":/icons/stop.png"))
- self.stopAction = toolBar.addAction(stopIcon, "Stop")
- self.stopAction.triggered.connect(self.player.stop)
- playMenu.addAction(self.stopAction)
+ self._stop_action = tool_bar.addAction(stopIcon, "Stop")
+ self._stop_action.triggered.connect(self._ensure_stopped)
+ play_menu.addAction(self._stop_action)
# many lines were omitted
--- /dev/null
+// Copyright (C) 2021 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+
+import QtQuick 2.12
+import QtQuick.Controls 2.12
+
+Page {
+ width: 640
+ height: 480
+ required property var myModel
+
+ header: Label {
+ color: "#15af15"
+ text: qsTr("Where do people use Qt?")
+ font.pointSize: 17
+ font.bold: true
+ font.family: "Arial"
+ renderType: Text.NativeRendering
+ horizontalAlignment: Text.AlignHCenter
+ padding: 10
+ }
+ Rectangle {
+ id: root
+ width: parent.width
+ height: parent.height
+
+ Image {
+ id: image
+ fillMode: Image.PreserveAspectFit
+ anchors.centerIn: root
+ source: "./logo.png"
+ opacity: 0.5
+
+ }
+
+ ListView {
+ id: view
+ anchors.fill: root
+ anchors.margins: 25
+ model: myModel
+ delegate: Text {
+ anchors.leftMargin: 50
+ font.pointSize: 15
+ horizontalAlignment: Text.AlignHCenter
+ text: display
+ }
+ }
+ }
+ NumberAnimation {
+ id: anim
+ running: true
+ target: view
+ property: "contentY"
+ duration: 500
+ }
+}
--- /dev/null
+module Main
+Main 254.0 Main.qml
# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+from __future__ import annotations
import sys
import urllib.request
import json
-from pathlib import Path
from PySide6.QtQuick import QQuickView
-from PySide6.QtCore import QStringListModel, QUrl
+from PySide6.QtCore import QStringListModel
from PySide6.QtGui import QGuiApplication
if __name__ == '__main__':
- #get our data
+ # get our data
url = "http://country.io/names.json"
response = urllib.request.urlopen(url)
data = json.loads(response.read().decode('utf-8'))
- #Format and sort the data
+ # Format and sort the data
data_list = list(data.values())
data_list.sort()
- #Set up the application window
+ # Set up the application window
app = QGuiApplication(sys.argv)
view = QQuickView()
view.setResizeMode(QQuickView.SizeRootObjectToView)
- #Expose the list to the Qml code
+ # Expose the list to the Qml code
my_model = QStringListModel()
my_model.setStringList(data_list)
view.setInitialProperties({"myModel": my_model})
- #Load the QML file
- qml_file = Path(__file__).parent / "view.qml"
- view.setSource(QUrl.fromLocalFile(qml_file.resolve()))
+ # Load the QML file
+ # Add the current directory to the import paths and load the main module.
+ view.engine().addImportPath(sys.path[0])
+ view.loadFromModule("Main", "Main")
- #Show the window
+ # Show the window
if view.status() == QQuickView.Error:
sys.exit(-1)
view.show()
- #execute and cleanup
+ # execute and cleanup
app.exec()
del view
This should create a ``main.py`` and ```main.pyproject`` files
for the project.
-#. Download :download:`view.qml<view.qml>` and :download:`logo.png <logo.png>`
- and move them to your project folder.
+#. Download :download:`Main.qml<Main/Main.qml>`, :download:`qmldir<Main/qmldir>`
+ and :download:`logo.png <Main/logo.png>` and place them in a subdirectory
+ named `Main` in your project folder. This creates a basic QML module.
#. Double-click on ``main.pyproject`` to open it in edit mode, and append
``view.qml`` and ``logo.png`` to the **files** list. This is how your
.. code::
{
- "files": ["main.py", "view.qml", "logo.png"]
+ "files": ["main.py", "Main/Main.qml", "Main/logo.png", "Main/qmldir"]
}
#. Now that you have the necessary bits for the application, import the
.. literalinclude:: main.py
:linenos:
- :lines: 3-23
- :emphasize-lines: 7-9,14-17
+ :lines: 5-23
+ :emphasize-lines: 5-7,12-15
#. Now, set up the application window using
:ref:`PySide6.QtGui.QGuiApplication<qguiapplication>`, which manages the application-wide
.. literalinclude:: main.py
:linenos:
- :lines: 3-28
- :emphasize-lines: 23-25
+ :lines: 5-28
+ :emphasize-lines: 21-24
.. note:: Setting the resize policy is important if you want the
root item to resize itself to fit the window or vice-a-versa.
.. literalinclude:: main.py
:linenos:
- :lines: 3-33
- :emphasize-lines: 28-31
+ :lines: 5-33
+ :emphasize-lines: 26-29
-#. Load the ``view.qml`` to the ``QQuickView`` and call ``show()`` to
+#. Load the ``Main.qml`` to the ``QQuickView`` and call ``show()`` to
display the application window.
.. literalinclude:: main.py
:linenos:
- :lines: 3-42
- :emphasize-lines: 33-40
+ :lines: 5-43
+ :emphasize-lines: 31-39
#. Finally, execute the application to start the event loop and clean up.
.. literalinclude:: main.py
:linenos:
- :lines: 3-
- :emphasize-lines: 42-44
+ :lines: 5-
+ :emphasize-lines: 41-43
#. Your application is ready to be run now. Select **Projects** mode to
choose the Python version to run it.
Related information
********************
-* `QML Reference <https://doc.qt.io/qt-5/qmlreference.html>`_
+* `QML Reference <https://doc.qt.io/qt-6/qmlreference.html>`_
* :doc:`../qmlintegration/qmlintegration`
+++ /dev/null
-// Copyright (C) 2021 The Qt Company Ltd.
-// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
-
-import QtQuick 2.12
-import QtQuick.Controls 2.12
-
-Page {
- width: 640
- height: 480
- required property var myModel
-
- header: Label {
- color: "#15af15"
- text: qsTr("Where do people use Qt?")
- font.pointSize: 17
- font.bold: true
- font.family: "Arial"
- renderType: Text.NativeRendering
- horizontalAlignment: Text.AlignHCenter
- padding: 10
- }
- Rectangle {
- id: root
- width: parent.width
- height: parent.height
-
- Image {
- id: image
- fillMode: Image.PreserveAspectFit
- anchors.centerIn: root
- source: "./logo.png"
- opacity: 0.5
-
- }
-
- ListView {
- id: view
- anchors.fill: root
- anchors.margins: 25
- model: myModel
- delegate: Text {
- anchors.leftMargin: 50
- font.pointSize: 15
- horizontalAlignment: Text.AlignHCenter
- text: display
- }
- }
- }
- NumberAnimation {
- id: anim
- running: true
- target: view
- property: "contentY"
- duration: 500
- }
-}
--- /dev/null
+// Copyright (C) 2021 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+
+
+import QtQuick 2.0
+import QtQuick.Layouts 1.11
+import QtQuick.Controls 2.1
+import QtQuick.Window 2.1
+import QtQuick.Controls.Material 2.1
+
+import io.qt.textproperties 1.0
+
+ApplicationWindow {
+ id: page
+ width: 800
+ height: 400
+ visible: true
+ Material.theme: Material.Dark
+ Material.accent: Material.Red
+
+ Bridge {
+ id: bridge
+ }
+
+ GridLayout {
+ id: grid
+ columns: 2
+ rows: 3
+
+ ColumnLayout {
+ spacing: 2
+ Layout.columnSpan: 1
+ Layout.preferredWidth: 400
+
+ Text {
+ id: leftlabel
+ Layout.alignment: Qt.AlignHCenter
+ color: "white"
+ font.pointSize: 16
+ text: "Qt for Python"
+ Layout.preferredHeight: 100
+ Material.accent: Material.Green
+ }
+
+ RadioButton {
+ id: italic
+ Layout.alignment: Qt.AlignLeft
+ text: "Italic"
+ onToggled: {
+ leftlabel.font.italic = bridge.getItalic(italic.text)
+ leftlabel.font.bold = bridge.getBold(italic.text)
+ leftlabel.font.underline = bridge.getUnderline(italic.text)
+
+ }
+ }
+ RadioButton {
+ id: bold
+ Layout.alignment: Qt.AlignLeft
+ text: "Bold"
+ onToggled: {
+ leftlabel.font.italic = bridge.getItalic(bold.text)
+ leftlabel.font.bold = bridge.getBold(bold.text)
+ leftlabel.font.underline = bridge.getUnderline(bold.text)
+ }
+ }
+ RadioButton {
+ id: underline
+ Layout.alignment: Qt.AlignLeft
+ text: "Underline"
+ onToggled: {
+ leftlabel.font.italic = bridge.getItalic(underline.text)
+ leftlabel.font.bold = bridge.getBold(underline.text)
+ leftlabel.font.underline = bridge.getUnderline(underline.text)
+ }
+ }
+ RadioButton {
+ id: noneradio
+ Layout.alignment: Qt.AlignLeft
+ text: "None"
+ checked: true
+ onToggled: {
+ leftlabel.font.italic = bridge.getItalic(noneradio.text)
+ leftlabel.font.bold = bridge.getBold(noneradio.text)
+ leftlabel.font.underline = bridge.getUnderline(noneradio.text)
+ }
+ }
+ }
+
+ ColumnLayout {
+ id: rightcolumn
+ spacing: 2
+ Layout.columnSpan: 1
+ Layout.preferredWidth: 400
+ Layout.preferredHeight: 400
+ Layout.fillWidth: true
+
+ RowLayout {
+ Layout.alignment: Qt.AlignVCenter | Qt.AlignHCenter
+
+
+ Button {
+ id: red
+ text: "Red"
+ highlighted: true
+ Material.accent: Material.Red
+ onClicked: {
+ leftlabel.color = bridge.getColor(red.text)
+ }
+ }
+ Button {
+ id: green
+ text: "Green"
+ highlighted: true
+ Material.accent: Material.Green
+ onClicked: {
+ leftlabel.color = bridge.getColor(green.text)
+ }
+ }
+ Button {
+ id: blue
+ text: "Blue"
+ highlighted: true
+ Material.accent: Material.Blue
+ onClicked: {
+ leftlabel.color = bridge.getColor(blue.text)
+ }
+ }
+ Button {
+ id: nonebutton
+ text: "None"
+ highlighted: true
+ Material.accent: Material.BlueGrey
+ onClicked: {
+ leftlabel.color = bridge.getColor(nonebutton.text)
+ }
+ }
+ }
+ RowLayout {
+ Layout.fillWidth: true
+ Layout.alignment: Qt.AlignVCenter | Qt.AlignHCenter
+ Text {
+ id: rightlabel
+ color: "white"
+ Layout.alignment: Qt.AlignLeft
+ text: "Font size"
+ Material.accent: Material.White
+ }
+ Slider {
+ width: rightcolumn.width*0.6
+ Layout.alignment: Qt.AlignRight
+ id: slider
+ value: 0.5
+ onValueChanged: {
+ leftlabel.font.pointSize = bridge.getSize(value)
+ }
+ }
+ }
+ }
+ }
+}
--- /dev/null
+module Main
+Main 254.0 Main.qml
# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+from __future__ import annotations
import sys
-from pathlib import Path
from PySide6.QtCore import QObject, Slot
from PySide6.QtGui import QGuiApplication
from PySide6.QtQml import QQmlApplicationEngine, QmlElement
from PySide6.QtQuickControls2 import QQuickStyle
-import style_rc
+import rc_style # noqa F401
# To be used on the @QmlElement decorator
# (QML_IMPORT_MINOR_VERSION is optional)
def getColor(self, s):
if s.lower() == "red":
return "#ef9a9a"
- elif s.lower() == "green":
+ if s.lower() == "green":
return "#a5d6a7"
- elif s.lower() == "blue":
+ if s.lower() == "blue":
return "#90caf9"
- else:
- return "white"
+ return "white"
@Slot(float, result=int)
def getSize(self, s):
size = int(s * 34)
- if size <= 0:
- return 1
- else:
- return size
+ return max(1, size)
@Slot(str, result=bool)
def getItalic(self, s):
- if s.lower() == "italic":
- return True
- else:
- return False
+ return s.lower() == "italic"
@Slot(str, result=bool)
def getBold(self, s):
- if s.lower() == "bold":
- return True
- else:
- return False
+ return s.lower() == "bold"
@Slot(str, result=bool)
def getUnderline(self, s):
- if s.lower() == "underline":
- return True
- else:
- return False
+ return s.lower() == "underline"
if __name__ == '__main__':
app = QGuiApplication(sys.argv)
QQuickStyle.setStyle("Material")
engine = QQmlApplicationEngine()
-
- # Get the path of the current directory, and then add the name
- # of the QML file, to load it.
- qml_file = Path(__file__).parent / 'view.qml'
- engine.load(qml_file)
+ # Add the current directory to the import paths and load the main module.
+ engine.addImportPath(sys.path[0])
+ engine.loadFromModule("Main", "Main")
if not engine.rootObjects():
sys.exit(-1)
- sys.exit(app.exec())
+ ex = app.exec()
+ del engine
+ sys.exit(ex)
.. literalinclude:: main.py
:linenos:
- :lines: 63-76
- :emphasize-lines: 4,9
+ :lines: 51-64
+ :emphasize-lines: 6,7
Notice that we only need a :code:`QQmlApplicationEngine` to
:code:`load` the QML file.
.. literalinclude:: main.py
:linenos:
- :lines: 14-54
+ :lines: 14-49
:emphasize-lines: 3,4,7
Notice that the registration happens thanks to the :code:`QmlElement`
This :code:`id` will help you to get a reference to the element
that was registered from Python.
- .. literalinclude:: view.qml
+ .. literalinclude:: Main/Main.qml
:linenos:
:lines: 45-55
:emphasize-lines: 6-8
will return *False*, that is how we make sure only one is being
applied to the text.
+#. Put the file into into a directory named :code:`Main` along
+ with a file named :code:`qmldir` to describe a basic QML module:
+
+ .. code-block:: text
+
+ module Main
+ Main 254.0 Main.qml
+
#. Each slot verifies if the selected option contains the text associated
to the property:
.. literalinclude:: main.py
:linenos:
- :lines: 42-47
- :emphasize-lines: 4,6
+ :lines: 42-44
Returning *True* or *False* allows you to activate and deactivate
the properties of the QML UI elements.
.. literalinclude:: main.py
:linenos:
- :lines: 34-39
+ :lines: 33-36
#. Now, for changing the look of our application, you have two options:
.. literalinclude:: style.qrc
:linenos:
- Generate the *rc* file running, ``pyside6-rcc style.qrc -o style_rc.py``
+ Generate the *rc* file running, ``pyside6-rcc style.qrc -o rc_style.py``
And finally import it from your ``main.py`` script.
.. literalinclude:: main.py
:linenos:
- :lines: 4-12
- :emphasize-lines: 9
+ :lines: 5-12
+ :emphasize-lines: 8
You can read more about this configuration file
`here <https://doc.qt.io/qt-5/qtquickcontrols2-configuration.html>`_.
.. image:: textproperties_material.png
-You can :download:`view.qml <view.qml>` and
-:download:`main.py <main.py>` to try this example.
+You can download :download:`Main.qml <Main/Main.qml>`,
+:download:`qmldir <Main/qmldir>` and :download:`main.py <main.py>`
+to try this example.
+++ /dev/null
-// Copyright (C) 2021 The Qt Company Ltd.
-// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
-
-
-import QtQuick 2.0
-import QtQuick.Layouts 1.11
-import QtQuick.Controls 2.1
-import QtQuick.Window 2.1
-import QtQuick.Controls.Material 2.1
-
-import io.qt.textproperties 1.0
-
-ApplicationWindow {
- id: page
- width: 800
- height: 400
- visible: true
- Material.theme: Material.Dark
- Material.accent: Material.Red
-
- Bridge {
- id: bridge
- }
-
- GridLayout {
- id: grid
- columns: 2
- rows: 3
-
- ColumnLayout {
- spacing: 2
- Layout.columnSpan: 1
- Layout.preferredWidth: 400
-
- Text {
- id: leftlabel
- Layout.alignment: Qt.AlignHCenter
- color: "white"
- font.pointSize: 16
- text: "Qt for Python"
- Layout.preferredHeight: 100
- Material.accent: Material.Green
- }
-
- RadioButton {
- id: italic
- Layout.alignment: Qt.AlignLeft
- text: "Italic"
- onToggled: {
- leftlabel.font.italic = bridge.getItalic(italic.text)
- leftlabel.font.bold = bridge.getBold(italic.text)
- leftlabel.font.underline = bridge.getUnderline(italic.text)
-
- }
- }
- RadioButton {
- id: bold
- Layout.alignment: Qt.AlignLeft
- text: "Bold"
- onToggled: {
- leftlabel.font.italic = bridge.getItalic(bold.text)
- leftlabel.font.bold = bridge.getBold(bold.text)
- leftlabel.font.underline = bridge.getUnderline(bold.text)
- }
- }
- RadioButton {
- id: underline
- Layout.alignment: Qt.AlignLeft
- text: "Underline"
- onToggled: {
- leftlabel.font.italic = bridge.getItalic(underline.text)
- leftlabel.font.bold = bridge.getBold(underline.text)
- leftlabel.font.underline = bridge.getUnderline(underline.text)
- }
- }
- RadioButton {
- id: noneradio
- Layout.alignment: Qt.AlignLeft
- text: "None"
- checked: true
- onToggled: {
- leftlabel.font.italic = bridge.getItalic(noneradio.text)
- leftlabel.font.bold = bridge.getBold(noneradio.text)
- leftlabel.font.underline = bridge.getUnderline(noneradio.text)
- }
- }
- }
-
- ColumnLayout {
- id: rightcolumn
- spacing: 2
- Layout.columnSpan: 1
- Layout.preferredWidth: 400
- Layout.preferredHeight: 400
- Layout.fillWidth: true
-
- RowLayout {
- Layout.alignment: Qt.AlignVCenter | Qt.AlignHCenter
-
-
- Button {
- id: red
- text: "Red"
- highlighted: true
- Material.accent: Material.Red
- onClicked: {
- leftlabel.color = bridge.getColor(red.text)
- }
- }
- Button {
- id: green
- text: "Green"
- highlighted: true
- Material.accent: Material.Green
- onClicked: {
- leftlabel.color = bridge.getColor(green.text)
- }
- }
- Button {
- id: blue
- text: "Blue"
- highlighted: true
- Material.accent: Material.Blue
- onClicked: {
- leftlabel.color = bridge.getColor(blue.text)
- }
- }
- Button {
- id: nonebutton
- text: "None"
- highlighted: true
- Material.accent: Material.BlueGrey
- onClicked: {
- leftlabel.color = bridge.getColor(nonebutton.text)
- }
- }
- }
- RowLayout {
- Layout.fillWidth: true
- Layout.alignment: Qt.AlignVCenter | Qt.AlignHCenter
- Text {
- id: rightlabel
- color: "white"
- Layout.alignment: Qt.AlignLeft
- text: "Font size"
- Material.accent: Material.White
- }
- Slider {
- width: rightcolumn.width*0.6
- Layout.alignment: Qt.AlignRight
- id: slider
- value: 0.5
- onValueChanged: {
- leftlabel.font.pointSize = bridge.getSize(value)
- }
- }
- }
- }
- }
-}
--- /dev/null
+// Copyright (C) 2021 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+
+import QtQuick
+import QtQuick.Layouts
+import QtQuick.Controls
+import ChatModel
+
+ApplicationWindow {
+ id: window
+ title: qsTr("Chat")
+ width: 640
+ height: 960
+ visible: true
+
+ SqlConversationModel {
+ id: chat_model
+ }
+
+ ColumnLayout {
+ anchors.fill: parent
+
+ ListView {
+ id: listView
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+ Layout.margins: pane.leftPadding + messageField.leftPadding
+ displayMarginBeginning: 40
+ displayMarginEnd: 40
+ verticalLayoutDirection: ListView.BottomToTop
+ spacing: 12
+ model: chat_model
+ delegate: Column {
+ anchors.right: sentByMe ? listView.contentItem.right : undefined
+ spacing: 6
+
+ readonly property bool sentByMe: model.recipient !== "Me"
+ Row {
+ id: messageRow
+ spacing: 6
+ anchors.right: sentByMe ? parent.right : undefined
+
+ Rectangle {
+ width: Math.min(messageText.implicitWidth + 24,
+ listView.width - (!sentByMe ? messageRow.spacing : 0))
+ height: messageText.implicitHeight + 24
+ radius: 15
+ color: sentByMe ? "lightgrey" : "steelblue"
+
+ Label {
+ id: messageText
+ text: model.message
+ color: sentByMe ? "black" : "white"
+ anchors.fill: parent
+ anchors.margins: 12
+ wrapMode: Label.Wrap
+ }
+ }
+ }
+
+ Label {
+ id: timestampText
+ text: Qt.formatDateTime(model.timestamp, "d MMM hh:mm")
+ color: "lightgrey"
+ anchors.right: sentByMe ? parent.right : undefined
+ }
+ }
+
+ ScrollBar.vertical: ScrollBar {}
+ }
+
+ Pane {
+ id: pane
+ Layout.fillWidth: true
+
+ RowLayout {
+ width: parent.width
+
+ TextArea {
+ id: messageField
+ Layout.fillWidth: true
+ placeholderText: qsTr("Compose message")
+ wrapMode: TextArea.Wrap
+ }
+
+ Button {
+ id: sendButton
+ text: qsTr("Send")
+ enabled: messageField.length > 0
+ onClicked: {
+ listView.model.send_message("machine", messageField.text, "Me");
+ messageField.text = "";
+ }
+ }
+ }
+ }
+ }
+}
--- /dev/null
+module Main
+Main 254.0 Main.qml
+++ /dev/null
-// Copyright (C) 2021 The Qt Company Ltd.
-// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
-
-import QtQuick
-import QtQuick.Layouts
-import QtQuick.Controls
-import ChatModel
-
-ApplicationWindow {
- id: window
- title: qsTr("Chat")
- width: 640
- height: 960
- visible: true
-
- SqlConversationModel {
- id: chat_model
- }
-
- ColumnLayout {
- anchors.fill: window
-
- ListView {
- id: listView
- Layout.fillWidth: true
- Layout.fillHeight: true
- Layout.margins: pane.leftPadding + messageField.leftPadding
- displayMarginBeginning: 40
- displayMarginEnd: 40
- verticalLayoutDirection: ListView.BottomToTop
- spacing: 12
- model: chat_model
- delegate: Column {
- anchors.right: sentByMe ? listView.contentItem.right : undefined
- spacing: 6
-
- readonly property bool sentByMe: model.recipient !== "Me"
- Row {
- id: messageRow
- spacing: 6
- anchors.right: sentByMe ? parent.right : undefined
-
- Rectangle {
- width: Math.min(messageText.implicitWidth + 24,
- listView.width - (!sentByMe ? messageRow.spacing : 0))
- height: messageText.implicitHeight + 24
- radius: 15
- color: sentByMe ? "lightgrey" : "steelblue"
-
- Label {
- id: messageText
- text: model.message
- color: sentByMe ? "black" : "white"
- anchors.fill: parent
- anchors.margins: 12
- wrapMode: Label.Wrap
- }
- }
- }
-
- Label {
- id: timestampText
- text: Qt.formatDateTime(model.timestamp, "d MMM hh:mm")
- color: "lightgrey"
- anchors.right: sentByMe ? parent.right : undefined
- }
- }
-
- ScrollBar.vertical: ScrollBar {}
- }
-
- Pane {
- id: pane
- Layout.fillWidth: true
-
- RowLayout {
- width: parent.width
-
- TextArea {
- id: messageField
- Layout.fillWidth: true
- placeholderText: qsTr("Compose message")
- wrapMode: TextArea.Wrap
- }
-
- Button {
- id: sendButton
- text: qsTr("Send")
- enabled: messageField.length > 0
- onClicked: {
- listView.model.send_message("machine", messageField.text, "Me");
- messageField.text = "";
- }
- }
- }
- }
- }
-}
# Copyright (C) 2022 The Qt Company Ltd.
-# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+from __future__ import annotations
import sys
import logging
-from PySide6.QtCore import QDir, QFile, QUrl
+from PySide6.QtCore import QCoreApplication, QDir, QFile, QStandardPaths
from PySide6.QtGui import QGuiApplication
from PySide6.QtQml import QQmlApplicationEngine
from PySide6.QtSql import QSqlDatabase
# We import the file just to trigger the QmlElement type registration.
-import sqlDialog
+import sqlDialog # noqa E703
logging.basicConfig(filename="chat.log", level=logging.DEBUG)
logger = logging.getLogger("logger")
if not database.isValid():
logger.error("Cannot add database")
- write_dir = QDir("")
+ app_data = QStandardPaths.writableLocation(QStandardPaths.StandardLocation.AppDataLocation)
+ write_dir = QDir(app_data)
if not write_dir.mkpath("."):
- logger.error("Failed to create writable directory")
+ logger.error(f"Failed to create writable directory {app_data}")
# Ensure that we have a writable location on all devices.
abs_path = write_dir.absolutePath()
if __name__ == "__main__":
app = QGuiApplication()
+ QCoreApplication.setOrganizationName("QtProject")
+ QCoreApplication.setApplicationName("Chat Tutorial")
+
connectToDatabase()
engine = QQmlApplicationEngine()
- engine.load(QUrl("chat.qml"))
+ engine.addImportPath(sys.path[0])
+ engine.loadFromModule("Main", "Main")
if not engine.rootObjects():
sys.exit(-1)
app.exec()
+ del engine
.. literalinclude:: sqlDialog.py
:linenos:
- :lines: 4-43
+ :lines: 5-44
The ``SqlConversationModel`` class offers the read-only data model required for the non-editable
contacts list. It derives from the :ref:`QSqlQueryModel` class, which is the logical choice for
.. literalinclude:: sqlDialog.py
:linenos:
- :lines: 47-59
-
-In ``setRecipient()``, you set a filter over the returned results from the database, and
-emit a signal every time the recipient of the message changes.
-
-.. literalinclude:: sqlDialog.py
- :linenos:
- :lines: 61-70
+ :lines: 48-60
The ``data()`` function falls back to ``QSqlTableModel``'s implementation if the role is not a
custom user role.
.. literalinclude:: sqlDialog.py
:linenos:
- :lines: 72-79
+ :lines: 62-69
In ``roleNames()``, we return a Python dictionary with our custom role and role names as key-values
pairs, so we can use these roles in QML.
Alternatively, it can be useful to declare an Enum to hold all of the role values.
-Note that ``names`` has to be a hash to be used as a dictionary key,
-and that's why we're using the ``hash`` function.
.. literalinclude:: sqlDialog.py
:linenos:
- :lines: 81-95
+ :lines: 71-78
The ``send_message()`` function uses the given recipient and message to insert a new record into
the database.
.. literalinclude:: sqlDialog.py
:linenos:
- :lines: 97-116
+ :lines: 80-99
-chat.qml
+Main.qml
--------
-Let's look at the ``chat.qml`` file.
+Let's look at the ``Main.qml`` file.
-.. literalinclude:: chat.qml
+.. literalinclude:: Main/Main.qml
:linenos:
:lines: 4-6
Among other things, this provides access to ``ApplicationWindow``, which replaces the existing
root type, Window:
-Let's step through the ``chat.qml`` file.
+Let's step through the ``Main/Main.qml`` file.
-.. literalinclude:: chat.qml
+.. literalinclude:: Main/Main.qml
:linenos:
:lines: 9-14
Because we are exposing the :code:`SqlConversationModel` class to QML, we will
declare a component to access it:
-.. literalinclude:: chat.qml
+.. literalinclude:: Main/Main.qml
:linenos:
:lines: 16-18
resizable user interfaces.
Below, we use `ColumnLayout`_ to vertically lay out a `ListView`_ and a `Pane`_.
- .. literalinclude:: chat.qml
+ .. literalinclude:: Main/Main.qml
:linenos:
:lines: 20-23
- .. literalinclude:: chat.qml
+ .. literalinclude:: Main/Main.qml
:linenos:
:lines: 72-74
Let's look at the ``Listview`` in detail:
-.. literalinclude:: chat.qml
+.. literalinclude:: Main/Main.qml
:linenos:
:lines: 23-70
button to send the message.
We use Pane to cover the area under these two items:
-.. literalinclude:: chat.qml
+.. literalinclude:: Main/Main.qml
:linenos:
:lines: 72-96
.. _displayMarginEnd: https://doc.qt.io/qt-5/qml-qtquick-listview.html#displayMarginEnd-prop
.. _TextArea: https://doc.qt.io/qt-5/qml-qtquick-controls2-textarea.html
+``Main.qml`` needs to be put into a directory named :code:`Main` along
+with a file named ``qmldir`` to describe a basic QML module:
+
+.. literalinclude:: Main/qmldir
main.py
-------
.. literalinclude:: main.py
:linenos:
- :lines: 4-16
+ :lines: 5-17
``connectToDatabase()`` creates a connection with the SQLite database, creating the actual file
if it doesn't already exist.
.. literalinclude:: main.py
:linenos:
- :lines: 19-39
+ :lines: 20-41
A few interesting things happen in the ``main`` function:
.. literalinclude:: main.py
:linenos:
- :lines: 42-52
+ :lines: 45-59
.. image:: example_list_view.png
# Copyright (C) 2022 The Qt Company Ltd.
-# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+from __future__ import annotations
import datetime
import logging
self.select()
logging.debug("Table was loaded successfully.")
- def setRecipient(self, recipient):
- if recipient == self.recipient:
- pass
-
- self.recipient = recipient
-
- filter_str = (f"(recipient = '{self.recipient}' AND author = 'Me') OR "
- f"(recipient = 'Me' AND author='{self.recipient}')")
- self.setFilter(filter_str)
- self.select()
-
def data(self, index, role):
if role < Qt.UserRole:
return QSqlTableModel.data(self, index, role)
def roleNames(self):
"""Converts dict to hash because that's the result expected
by QSqlTableModel"""
- names = {}
- author = "author".encode()
- recipient = "recipient".encode()
- timestamp = "timestamp".encode()
- message = "message".encode()
-
- names[hash(Qt.UserRole)] = author
- names[hash(Qt.UserRole + 1)] = recipient
- names[hash(Qt.UserRole + 2)] = timestamp
- names[hash(Qt.UserRole + 3)] = message
-
- return names
+
+ return {int(Qt.UserRole): b"author",
+ Qt.UserRole + 1: b"recipient",
+ Qt.UserRole + 2: b"timestamp",
+ Qt.UserRole + 3: b"message"}
# This is a workaround because PySide doesn't provide Q_INVOKABLE
# So we declare this as a Slot to be able to call it from QML
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
#include "dynamicqmetaobject.h"
+#include "pysidelogging_p.h"
#include "pysideqobject.h"
#include "pysidesignal.h"
#include "pysidesignal_p.h"
return notifyId;
}
+static QByteArray msgInvalidPropertyType(const QByteArray &className,
+ const QByteArray &propertyName,
+ const QByteArray &propertyType)
+{
+ return "QMetaObjectBuilder: Failed to add property \""_ba + propertyName
+ + "\" to \""_ba + className + "\": Invalid property type \""
+ + propertyType + "\"."_ba;
+}
+
QMetaPropertyBuilder
MetaObjectBuilderPrivate::createProperty(PySideProperty *property,
const QByteArray &propertyName)
}
}
}
- return builder->addProperty(propertyName, property->d->typeName,
- propertyNotifyId);
+ const auto metaType = QMetaType::fromName(property->d->typeName);
+ if (!metaType.isValid()) {
+ const auto &msg = msgInvalidPropertyType(m_builder->className(), propertyName,
+ property->d->typeName);
+ PyErr_WarnEx(PyExc_RuntimeWarning, msg.constData(), 0);
+ }
+ return builder->addProperty(propertyName, property->d->typeName, metaType, propertyNotifyId);
}
int MetaObjectBuilderPrivate::addProperty(const QByteArray &propertyName,
if (PyMethod_Check(callback)) {
m_isMethod = true;
- // To avoid increment instance reference keep the callback information
+ // A method given by "signal.connect(foo.method)" is a temporarily created
+ // callable/partial function where self is bound as a first parameter.
+ // It can be split into self and the function. Keeping a reference on
+ // the callable itself would prevent object deletion. Instead, keep a
+ // reference on the function and listen for destruction of the object
+ // using a weak reference with notification.
m_callback = PyMethod_GET_FUNCTION(callback);
Py_INCREF(m_callback);
m_pythonSelf = PyMethod_GET_SELF(callback);
- //monitor class from method lifetime
m_weakRef = WeakRef::create(m_pythonSelf, DynamicSlotDataV2::onCallbackDestroyed, this);
} else if (PySide::isCompiledMethod(callback)) {
// PYSIDE-1523: PyMethod_Check is not accepting compiled form, we just go by attributes.
#include <optional>
#include <typeinfo>
+#ifdef Q_OS_WIN
+# include <conio.h>
+#else
+# include <QtCore/QDeadlineTimer>
+# include <QtCore/private/qcore_unix_p.h>
+#endif
+
using namespace Qt::StringLiterals;
static QStack<PySide::CleanupFunction> cleanupFunctionList;
PySide::Feature::Enable(true);
}
+extern "C" {
+static int qAppInputHook()
+{
+ auto *app = QCoreApplication::instance();
+ if (app == nullptr || app->thread() != QThread::currentThread())
+ return 0;
+#ifndef Q_OS_WIN
+ // Check for press on stdin (file descriptor 0)
+ pollfd stdinPfd = qt_make_pollfd(0, POLLIN);
+ while (qt_safe_poll(&stdinPfd, 1, QDeadlineTimer{1}) == 0)
+ QCoreApplication::processEvents({}, 50000);
+#else
+ while (_kbhit() == 0)
+ QCoreApplication::processEvents({}, 50000);
+#endif
+ return 0;
+}
+} // extern "C"
+
+static void unregisterQAppInputHook()
+{
+ PyOS_InputHook = nullptr;
+}
+
+static void registerQAppInputHook()
+{
+ PyOS_InputHook = qAppInputHook;
+ qAddPostRoutine(unregisterQAppInputHook);
+}
+
void initQApp()
{
/*
* I would appreciate very much if someone could explain or even fix
* this issue. It exists only when a pre-existing application exists.
*/
- if (!qApp)
+ if (qApp == nullptr) {
+ registerQAppInputHook();
Py_DECREF(MakeQAppWrapper(nullptr));
+ }
// PYSIDE-1470: Register a function to destroy an application from shiboken.
setDestroyQApplication(destroyQCoreApplication);
static inline void warnDisconnectFailed(PyObject *aSlot, const QByteArray &signature)
{
if (PyErr_Occurred() != nullptr) { // avoid "%S" invoking str() when an error is set.
+ PyObject *exc{}, *inst{}, *tb{};
+ PyErr_Fetch(&exc, &inst, &tb);
PyErr_WarnFormat(PyExc_RuntimeWarning, 0, "Failed to disconnect (%s) from signal \"%s\".",
Py_TYPE(aSlot)->tp_name, signature.constData());
+ PyErr_Restore(exc, inst, tb);
} else {
PyErr_WarnFormat(PyExc_RuntimeWarning, 0, "Failed to disconnect (%S) from signal \"%s\".",
aSlot, signature.constData());
#include <QtCore/QVariant>
#include <string_view>
+#include <utility>
using namespace Qt::StringLiterals;
return ok;
}
+static std::pair<int, int> pythonVersion()
+{
+ // read environment set by pyside_tool.py
+ bool majorOk{};
+ bool minorOk{};
+ const int majorVersion = qEnvironmentVariableIntValue("PY_MAJOR_VERSION", &majorOk);
+ const int minorVersion = qEnvironmentVariableIntValue("PY_MINOR_VERSION", &minorOk);
+ if (majorOk && minorVersion)
+ return {majorVersion, minorVersion};
+ return {PY_MAJOR_VERSION, PY_MINOR_VERSION};
+}
+
static void initVirtualEnvironment()
{
static const char virtualEnvVar[] = "VIRTUAL_ENV";
- // As of Python 3.8/Windows, Python is no longer able to run stand-alone in
- // a virtualenv due to missing libraries. Add the path to the modules
- // instead. macOS seems to be showing the same issues.
+ // Since Python 3.8 (Windows, macOS), Python is no longer able to run stand
+ // -alone in a virtualenv due to missing libraries. Add the path to the modules
+ // instead.
const auto os = QOperatingSystemVersion::currentType();
- bool ok;
- int majorVersion = qEnvironmentVariableIntValue("PY_MAJOR_VERSION", &ok);
- int minorVersion = qEnvironmentVariableIntValue("PY_MINOR_VERSION", &ok);
- if (!ok) {
- majorVersion = PY_MAJOR_VERSION;
- minorVersion = PY_MINOR_VERSION;
- }
-
if (!qEnvironmentVariableIsSet(virtualEnvVar)
- || (os != QOperatingSystemVersion::MacOS && os != QOperatingSystemVersion::Windows)
- || (majorVersion == 3 && minorVersion < 8)) {
+ || (os != QOperatingSystemVersion::MacOS && os != QOperatingSystemVersion::Windows)) {
return;
}
case QOperatingSystemVersion::Windows:
pythonPath.append(virtualEnvPath + R"(\Lib\site-packages)");
break;
- case QOperatingSystemVersion::MacOS:
+ case QOperatingSystemVersion::MacOS: {
+ const auto version = pythonVersion();
pythonPath.append(virtualEnvPath + "/lib/python"_ba +
- QByteArray::number(majorVersion) + '.'
- + QByteArray::number(minorVersion)
+ QByteArray::number(version.first) + '.'
+ + QByteArray::number(version.second)
+ "/site-packages"_ba);
+ }
break;
default:
break;
--- /dev/null
+# Copyright (C) 2024 The Qt Company Ltd.
+# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
+from __future__ import annotations
+
+'''Test cases for QtAsyncio'''
+
+import unittest
+import asyncio
+
+import PySide6.QtAsyncio as QtAsyncio
+
+
+class QAsyncioTestCaseBug2790(unittest.TestCase):
+
+ async def producer(self, products: list[str]):
+ while True:
+ products.append("product")
+ await asyncio.sleep(2)
+
+ async def task(self, outputs: list[str]):
+ products = []
+ asyncio.ensure_future(self.producer(products))
+ for _ in range(6):
+ try:
+ async with asyncio.timeout(0.5):
+ while len(products) == 0:
+ await asyncio.sleep(0)
+ outputs.append(products.pop(0))
+ except TimeoutError:
+ outputs.append("Timeout")
+
+ def test_timeout(self):
+ # The Qt event loop (and thus QtAsyncio) does not guarantee that events
+ # will be processed in the order they were posted, so there is two
+ # possible outputs for this test.
+ outputs_expected_1 = ["product", "Timeout", "Timeout", "Timeout", "Timeout", "product"]
+ outputs_expected_2 = ["product", "Timeout", "Timeout", "Timeout", "product", "Timeout"]
+
+ outputs_real = []
+
+ QtAsyncio.run(self.task(outputs_real), keep_running=False)
+
+ self.assertTrue(outputs_real in [outputs_expected_1, outputs_expected_2])
+
+
+if __name__ == '__main__':
+ unittest.main()
--- /dev/null
+# Copyright (C) 2024 The Qt Company Ltd.
+# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
+from __future__ import annotations
+
+"""Test cases for QtAsyncio"""
+
+import unittest
+import asyncio
+import sys
+
+import PySide6.QtAsyncio as QtAsyncio
+
+
+@unittest.skipIf(sys.version_info < (3, 11), "Requires ExceptionGroup")
+class QAsyncioTestCaseBug2799(unittest.TestCase):
+ async def job(self):
+ await asyncio.sleep(1)
+
+ async def main(self):
+ async with asyncio.TaskGroup() as tg:
+ tg.create_task(self.job())
+ raise RuntimeError()
+
+ def test_exception_group(self):
+ with self.assertRaises(ExceptionGroup):
+ QtAsyncio.run(self.main(), keep_running=False)
+
+
+if __name__ == "__main__":
+ unittest.main()
import asyncio
import unittest
+import sys
import PySide6.QtAsyncio as QtAsyncio
+@unittest.skipIf(sys.version_info < (3, 11), "Requires ExceptionGroup")
class QAsyncioTestCaseCancelTaskGroup(unittest.TestCase):
+
+ """
+ PYSIDE-2644: If a task was cancelled, then a new future created from this
+ task should be cancelled as well. Otherwise, in some scenarios like a loop
+ inside the task and with bad timing, if the new future is not cancelled,
+ the task would continue running in this loop despite having been cancelled.
+ This bad timing can occur especially if the first future finishes very
+ quickly.
+ """
+
def setUp(self) -> None:
super().setUp()
# We only reach the end of the loop if the task is not cancelled.
for coro in coros:
try:
QtAsyncio.run(self.main(coro), keep_running=False)
- except ExceptionGroup as e:
+ except ExceptionGroup as e: # noqa: F821
self.assertEqual(len(e.exceptions), 1)
self.assertIsInstance(e.exceptions[0], RuntimeError)
self.assertFalse(self._loop_end_reached)
--- /dev/null
+# Copyright (C) 2024 The Qt Company Ltd.
+# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
+from __future__ import annotations
+
+"""Test cases for QtAsyncio"""
+
+import unittest
+import asyncio
+
+import PySide6.QtAsyncio as QtAsyncio
+
+
+class QAsyncioTestCaseUncancel(unittest.TestCase):
+ """ https://superfastpython.com/asyncio-cancel-task-cancellation """
+
+ async def worker(self, outputs: list[str]):
+ # Ensure the task always gets done.
+ while True:
+ try:
+ await asyncio.sleep(2)
+ outputs.append("Task sleep completed normally")
+ break
+ except asyncio.CancelledError:
+ outputs.append("Task is cancelled, ignore and try again")
+ asyncio.current_task().uncancel()
+
+ async def main(self, outputs: list[str]):
+ task = asyncio.create_task(self.worker(outputs))
+ # Allow the task to run briefly.
+ await asyncio.sleep(0.5)
+ task.cancel()
+ try:
+ await task
+ except asyncio.CancelledError:
+ outputs.append("Task was cancelled")
+
+ cancelling = task.cancelling()
+ self.assertEqual(cancelling, 0)
+ outputs.append(f"Task cancelling: {cancelling}")
+
+ cancelled = task.cancelled()
+ self.assertFalse(cancelled)
+ outputs.append(f"Task cancelled: {cancelled}")
+
+ done = task.done()
+ self.assertTrue(done)
+ outputs.append(f"Task done: {done}")
+
+ def test_uncancel(self):
+ outputs_expected = []
+ outputs_real = []
+
+ asyncio.run(self.main(outputs_real))
+ QtAsyncio.run(self.main(outputs_expected), keep_running=False)
+
+ self.assertIsNotNone(outputs_real)
+ self.assertEqual(outputs_real, outputs_expected)
+
+
+if __name__ == "__main__":
+ unittest.main()
self.assertTrue(s1.isValid())
self.assertEqual(s1.name(), "text/plain")
- krita = db.mimeTypeForName("application/x-krita")
- self.assertTrue(krita.isValid())
+ # Removed because of the move of to the Tika mimetypes.
+ # krita = db.mimeTypeForName("application/x-krita")
+ # self.assertTrue(krita.isValid())
rdf = db.mimeTypeForName("application/rdf+xml")
self.assertTrue(rdf.isValid())
self.assertEqual(rdf.name(), "application/rdf+xml")
self.assertTrue(rdf.comment())
if "en" in QLocale().name():
- self.assertEqual(rdf.comment(), "RDF file")
+ self.assertTrue(rdf.comment() in ("RDF file", "XML syntax for RDF graphs"))
bzip2 = db.mimeTypeForName("application/x-bzip2")
self.assertTrue(bzip2.isValid())
return
size = 256
byte_array = QByteArray(size, '7')
- buffer = QAudioBuffer(byte_array, self._devices[0].preferredFormat())
+ device = self._devices[0]
+ format = device.preferredFormat()
+ # Observed to be "Unknown" on Linux
+ if format.sampleFormat() == QAudioFormat.SampleFormat.Unknown:
+ sample_formats = device.supportedSampleFormats()
+ if sample_formats:
+ format.setSampleFormat(sample_formats[0])
+ format.setSampleRate(48000)
+ buffer = QAudioBuffer(byte_array, format)
self.assertEqual(buffer.byteCount(), 256)
data = buffer.data()
actual_byte_array = QByteArray(bytearray(data))
--- /dev/null
+# Copyright (C) 2023 The Qt Company Ltd.
+# SPDX-License-Identifier: BSD-3-Clause
+
+# Tests to be added later
init_test_paths(False)
from PySide6.QtCore import Signal
-from PySide6.QtWidgets import QApplication, QWidget
-from PySide6 import QtWidgets
+from PySide6.QtWidgets import QApplication, QWidget # noqa F401
+
+from helper.usesqapplication import UsesQApplication
class Harness(QWidget):
self.method___result = self.sender()
-class TestMangle(unittest.TestCase):
-
- def setUp(self):
- QApplication()
-
- def tearDown(self):
- qApp.shutdown()
+class TestMangle(UsesQApplication):
def testPrivateMangle(self):
harness = Harness()
self.assertEqual(config_obj.get_value("app", "project_dir"), ".")
self.assertEqual(config_obj.get_value("app", "exec_directory"), ".")
self.assertEqual(config_obj.get_value("python", "packages"),
- "Nuitka==2.3.2")
+ "Nuitka==2.4.8")
self.assertEqual(config_obj.get_value("qt", "qml_files"), "")
equ_base = "--quiet --noinclude-qt-translations"
equ_value = equ_base + " --static-libpython=no" if is_pyenv_python() else equ_base
self.deploy.main(main_file=fake_main_file, config_file=self.config_file)
self.assertTrue("Directory does not contain main.py file." in str(context.exception))
+ def testStandaloneMode(self, mock_plugins):
+ mock_plugins.return_value = self.all_plugins
+ # remove --onefile from self.expected_run_cmd and replace it with --standalone
+ self.expected_run_cmd = self.expected_run_cmd.replace(" --onefile", " --standalone")
+ # test standalone mode
+ original_output = self.deploy.main(self.main_file, mode="standalone", dry_run=True,
+ force=True)
+
+ self.assertEqual(original_output, self.expected_run_cmd)
+
@unittest.skipIf(sys.platform == "darwin" and int(platform.mac_ver()[0].split('.')[0]) <= 11,
"Test only works on macOS version 12+")
self.assertEqual(config_obj.get_value("app", "project_dir"), ".")
self.assertEqual(config_obj.get_value("app", "exec_directory"), ".")
self.assertEqual(config_obj.get_value("python", "packages"),
- "Nuitka==2.3.2")
+ "Nuitka==2.4.8")
self.assertEqual(config_obj.get_value("qt", "qml_files"), "main.qml,MovingRectangle.qml")
equ_base = "--quiet --noinclude-qt-translations"
equ_value = equ_base + " --static-libpython=no" if is_pyenv_python() else equ_base
self.assertEqual(obtained_modules, expected_modules)
+@unittest.skipIf(sys.platform != "win32", "Test only works on Windows")
+class TestLongCommand(DeployTestBase):
+ @classmethod
+ def setUpClass(cls):
+ super().setUpClass()
+ example_qml = cls.example_root / "qml" / "editingmodel"
+ cls.temp_example_qml = Path(
+ shutil.copytree(example_qml, Path(cls.temp_dir) / "editingmodel")
+ ).resolve()
+
+ def setUp(self):
+ os.chdir(self.temp_example_qml)
+ self.main_file = self.temp_example_qml / "main.py"
+
+ @patch('deploy_lib.nuitka_helper.os.remove')
+ @patch("deploy_lib.config.run_qmlimportscanner")
+ @patch('deploy.DesktopConfig.qml_files', new_callable=mock.PropertyMock)
+ def test_main_with_mocked_qml_files(self, mock_qml_files, mock_qmlimportscanner, mock_remove):
+ mock_qmlimportscanner.return_value = ["QtQuick"]
+ mock_qml_files.return_value = [self.temp_example_qml / "MovingRectangle.qml"
+ for _ in range(500)]
+
+ command_str = self.deploy.main(self.main_file, force=True, keep_deployment_files=True,
+ dry_run=True)
+ mock_remove.assert_called_once()
+
+ # check if command_str ends with deploy_main.py
+ self.assertTrue(command_str.endswith("deploy_main.py"))
+
+ # check if deploy_main.py startes with # nuitka-project:
+ with open(self.temp_example_qml / "deploy_main.py", "r") as file:
+ # check if 517 lines start with # nuitka-project:
+ self.assertEqual(len([line for line in file.readlines()
+ if line.startswith("# nuitka-project:")]), 517)
+
+
if __name__ == "__main__":
unittest.main()
set(shiboken_MAJOR_VERSION "6")
set(shiboken_MINOR_VERSION "7")
-set(shiboken_MICRO_VERSION "2")
+set(shiboken_MICRO_VERSION "3")
set(shiboken_PRE_RELEASE_VERSION_TYPE "")
set(shiboken_PRE_RELEASE_VERSION "")
return str;
}
-static void applyCachedFunctionModifications(AbstractMetaFunction *metaFunction,
+static void applyCachedFunctionModifications(const AbstractMetaFunctionPtr &metaFunction,
const FunctionModificationList &functionMods)
{
for (const FunctionModification &mod : functionMods) {
return;
}
- AbstractMetaFunction *metaFunction = traverseFunction(item, baseoperandClass);
+ auto metaFunction = traverseFunction(item, baseoperandClass);
if (metaFunction == nullptr)
return;
}
metaFunction->setFlags(flags);
metaFunction->setAccess(Access::Public);
- AbstractMetaClass::addFunction(baseoperandClass, AbstractMetaFunctionCPtr(metaFunction));
+ AbstractMetaClass::addFunction(baseoperandClass, metaFunction);
if (!metaFunction->arguments().isEmpty()) {
const auto include = metaFunction->arguments().constFirst().type().typeEntry()->include();
baseoperandClass->typeEntry()->addArgumentInclude(include);
if (streamedClass == nullptr)
return false;
- AbstractMetaFunction *streamFunction = traverseFunction(item, streamedClass);
+ auto streamFunction = traverseFunction(item, streamedClass);
if (!streamFunction)
return false;
funcClass = streamClass;
}
- AbstractMetaClass::addFunction(funcClass, AbstractMetaFunctionCPtr(streamFunction));
+ AbstractMetaClass::addFunction(funcClass, streamFunction);
auto funcTe = funcClass->typeEntry();
if (funcClass == streamClass)
funcTe->addArgumentInclude(streamedClass->typeEntry()->include());
if (!funcEntry || !funcEntry->generateCode())
continue;
- AbstractMetaFunction *metaFunc = traverseFunction(func, nullptr);
- if (!metaFunc)
+ auto metaFuncPtr = traverseFunction(func, nullptr);
+ if (!metaFuncPtr)
continue;
- AbstractMetaFunctionCPtr metaFuncPtr(metaFunc);
- if (!funcEntry->hasSignature(metaFunc->minimalSignature()))
+ if (!funcEntry->hasSignature(metaFuncPtr->minimalSignature()))
continue;
- metaFunc->setTypeEntry(funcEntry);
- applyFunctionModifications(metaFunc);
- metaFunc->applyTypeModifications();
+ metaFuncPtr->setTypeEntry(funcEntry);
+ applyFunctionModifications(metaFuncPtr);
+ metaFuncPtr->applyTypeModifications();
setInclude(funcEntry, func->fileName());
}
}
-void AbstractMetaBuilderPrivate::fixReturnTypeOfConversionOperator(AbstractMetaFunction *metaFunction)
+void AbstractMetaBuilderPrivate::fixReturnTypeOfConversionOperator(const AbstractMetaFunctionPtr &metaFunction)
{
if (!metaFunction->isConversionOperator())
return;
metaFunction->setType(metaType);
}
-AbstractMetaFunctionRawPtrList
+AbstractMetaFunctionList
AbstractMetaBuilderPrivate::classFunctionList(const ScopeModelItem &scopeItem,
AbstractMetaClass::Attributes *constructorAttributes,
const AbstractMetaClassPtr ¤tClass)
{
*constructorAttributes = {};
- AbstractMetaFunctionRawPtrList result;
+ AbstractMetaFunctionList result;
const FunctionList &scopeFunctionList = scopeItem->functions();
result.reserve(scopeFunctionList.size());
const bool isNamespace = currentClass->isNamespace();
} else if (function->isSpaceshipOperator() && !function->isDeleted()) {
if (currentClass)
AbstractMetaClass::addSynthesizedComparisonOperators(currentClass);
- } else if (auto *metaFunction = traverseFunction(function, currentClass)) {
+ } else if (auto metaFunction = traverseFunction(function, currentClass)) {
result.append(metaFunction);
} else if (!function->isDeleted() && function->functionType() == CodeModel::Constructor) {
auto arguments = function->arguments();
const AbstractMetaClassPtr &metaClass)
{
AbstractMetaClass::Attributes constructorAttributes;
- const AbstractMetaFunctionRawPtrList functions =
+ const AbstractMetaFunctionList functions =
classFunctionList(scopeItem, &constructorAttributes, metaClass);
metaClass->setAttributes(metaClass->attributes() | constructorAttributes);
- for (AbstractMetaFunction *metaFunction : functions) {
+ for (const auto &metaFunction : functions) {
if (metaClass->isNamespace())
metaFunction->setCppAttribute(FunctionAttribute::Static);
if (!metaFunction->isDestructor()
&& !(metaFunction->isPrivate() && metaFunction->functionType() == AbstractMetaFunction::ConstructorFunction)) {
- if (metaFunction->isSignal() && metaClass->hasSignal(metaFunction))
- qCWarning(lcShiboken, "%s", qPrintable(msgSignalOverloaded(metaClass, metaFunction)));
+ if (metaFunction->isSignal() && metaClass->hasSignal(metaFunction.get()))
+ qCWarning(lcShiboken, "%s", qPrintable(msgSignalOverloaded(metaClass,
+ metaFunction.get())));
if (metaFunction->isConversionOperator())
fixReturnTypeOfConversionOperator(metaFunction);
- AbstractMetaClass::addFunction(metaClass, AbstractMetaFunctionCPtr(metaFunction));
+ AbstractMetaClass::addFunction(metaClass, metaFunction);
applyFunctionModifications(metaFunction);
} else if (metaFunction->isDestructor()) {
metaClass->setHasPrivateDestructor(metaFunction->isPrivate());
metaClass->setHasProtectedDestructor(metaFunction->isProtected());
metaClass->setHasVirtualDestructor(metaFunction->isVirtual());
}
- if (!metaFunction->ownerClass()) {
- delete metaFunction;
- metaFunction = nullptr;
- }
}
fillAddedFunctions(metaClass);
return result;
}
-void AbstractMetaBuilderPrivate::applyFunctionModifications(AbstractMetaFunction *func)
+void AbstractMetaBuilderPrivate::applyFunctionModifications(const AbstractMetaFunctionPtr &func)
{
AbstractMetaFunction& funcRef = *func;
for (const FunctionModification &mod : func->modifications(func->implementingClass())) {
bool AbstractMetaBuilderPrivate::traverseAddedGlobalFunction(const AddedFunctionPtr &addedFunc,
QString *errorMessage)
{
- AbstractMetaFunction *metaFunction =
- traverseAddedFunctionHelper(addedFunc, nullptr, errorMessage);
+ auto metaFunction = traverseAddedFunctionHelper(addedFunc, nullptr, errorMessage);
if (metaFunction == nullptr)
return false;
- m_globalFunctions << AbstractMetaFunctionCPtr(metaFunction);
+ m_globalFunctions << metaFunction;
return true;
}
-AbstractMetaFunction *
+AbstractMetaFunctionPtr
AbstractMetaBuilderPrivate::traverseAddedFunctionHelper(const AddedFunctionPtr &addedFunc,
const AbstractMetaClassPtr &metaClass /* = {} */,
QString *errorMessage)
msgAddedFunctionInvalidReturnType(addedFunc->name(),
addedFunc->returnType().qualifiedName(),
*errorMessage, metaClass);
- return nullptr;
+ return {};
}
- auto *metaFunction = new AbstractMetaFunction(addedFunc);
+ auto metaFunction = std::make_shared<AbstractMetaFunction>(addedFunc);
metaFunction->setType(returnType.value());
metaFunction->setFunctionType(functionTypeFromName(addedFunc->name()));
msgAddedFunctionInvalidArgType(addedFunc->name(),
arg.typeInfo.qualifiedName(), i + 1,
*errorMessage, metaClass);
- delete metaFunction;
- return nullptr;
+ return {};
}
type->decideUsagePattern();
const AbstractMetaClassPtr &metaClass,
QString *errorMessage)
{
- AbstractMetaFunction *metaFunction =
- traverseAddedFunctionHelper(addedFunc, metaClass, errorMessage);
+ auto metaFunction = traverseAddedFunctionHelper(addedFunc, metaClass, errorMessage);
if (metaFunction == nullptr)
return false;
metaFunction->setDeclaringClass(metaClass);
metaFunction->setImplementingClass(metaClass);
- AbstractMetaClass::addFunction(metaClass, AbstractMetaFunctionCPtr(metaFunction));
+ AbstractMetaClass::addFunction(metaClass, metaFunction);
metaClass->setHasNonPrivateConstructor(true);
return true;
}
-void AbstractMetaBuilderPrivate::fixArgumentNames(AbstractMetaFunction *func, const FunctionModificationList &mods)
+void AbstractMetaBuilderPrivate::fixArgumentNames(const AbstractMetaFunctionPtr &func,
+ const FunctionModificationList &mods)
{
AbstractMetaArgumentList &arguments = func->arguments();
// Apply the <array> modifications of the arguments
static bool applyArrayArgumentModifications(const FunctionModificationList &functionMods,
- AbstractMetaFunction *func,
+ const AbstractMetaFunctionPtr &func,
QString *errorMessage)
{
for (const FunctionModification &mod : functionMods) {
m_rejectedFunctions.insert({reason, signatureWithType, sortKey, rejectReason});
}
-AbstractMetaFunction *AbstractMetaBuilderPrivate::traverseFunction(const FunctionModelItem &functionItem,
- const AbstractMetaClassPtr ¤tClass)
+AbstractMetaFunctionPtr
+ AbstractMetaBuilderPrivate::traverseFunction(const FunctionModelItem &functionItem,
+ const AbstractMetaClassPtr ¤tClass)
{
const auto *tdb = TypeDatabase::instance();
if (!functionItem->templateParameters().isEmpty())
- return nullptr;
+ return {};
if (functionItem->isDeleted()) {
switch (functionItem->functionType()) {
default:
break;
}
- return nullptr;
+ return {};
}
const QString &functionName = functionItem->name();
const QString className = currentClass != nullptr ?
// Skip enum helpers generated by Q_ENUM
if ((currentClass == nullptr || currentClass->isNamespace())
&& (functionName == u"qt_getEnumMetaObject" || functionName == u"qt_getEnumName")) {
- return nullptr;
+ return {};
}
// Clang: Skip qt_metacast(), qt_metacall(), expanded from Q_OBJECT
if (currentClass != nullptr) {
if (functionName == u"qt_check_for_QGADGET_macro"
|| functionName.startsWith(u"qt_meta")) {
- return nullptr;
+ return {};
}
if (functionName == u"metaObject" && className != u"QObject")
- return nullptr;
+ return {};
}
} // PySide extensions
if (tdb->isFunctionRejected(className, functionName, &rejectReason)) {
rejectFunction(functionItem, currentClass,
AbstractMetaBuilder::GenerationDisabled, rejectReason);
- return nullptr;
+ return {};
}
const QString &signature = functionSignature(functionItem);
qCInfo(lcShiboken, "%s::%s was rejected by the type database (%s).",
qPrintable(className), qPrintable(signature), qPrintable(rejectReason));
}
- return nullptr;
+ return {};
}
if (functionItem->isFriend())
- return nullptr;
+ return {};
const auto cppAttributes = functionItem->attributes();
const bool deprecated = cppAttributes.testFlag(FunctionAttribute::Deprecated);
if (deprecated && m_skipDeprecated) {
rejectFunction(functionItem, currentClass,
AbstractMetaBuilder::GenerationDisabled, u" is deprecated."_s);
- return nullptr;
+ return {};
}
AbstractMetaFunction::Flags flags;
- auto *metaFunction = new AbstractMetaFunction(functionName);
+ auto metaFunction = std::make_shared<AbstractMetaFunction>(functionName);
metaFunction->setCppAttributes(cppAttributes);
const QByteArray cSignature = signature.toUtf8();
const QString unresolvedSignature =
if (tdb->isReturnTypeRejected(className, returnType.toString(), &rejectReason)) {
rejectFunction(functionItem, currentClass,
AbstractMetaBuilder::GenerationDisabled, rejectReason);
- delete metaFunction;
- return nullptr;
+ return {};
}
TranslateTypeFlags flags;
qPrintable(msgSkippingFunction(functionItem, signature, reason)));
rejectFunction(functionItem, currentClass,
AbstractMetaBuilder::UnmatchedReturnType, reason);
- delete metaFunction;
- return nullptr;
+ return {};
}
metaFunction->setType(type.value());
if (tdb->isArgumentTypeRejected(className, arg->type().toString(), &rejectReason)) {
rejectFunction(functionItem, currentClass,
AbstractMetaBuilder::GenerationDisabled, rejectReason);
- delete metaFunction;
- return nullptr;
+ return {};
}
TranslateTypeFlags flags;
qPrintable(msgSkippingFunction(functionItem, signature, reason)));
rejectFunction(functionItem, currentClass,
AbstractMetaBuilder::UnmatchedArgumentType, reason);
- delete metaFunction;
- return nullptr;
+ return {};
}
auto metaType = metaTypeO.value();
AbstractMetaArgumentList &metaArguments = metaFunction->arguments();
const FunctionModificationList functionMods = currentClass
- ? AbstractMetaFunction::findClassModifications(metaFunction, currentClass)
- : AbstractMetaFunction::findGlobalModifications(metaFunction);
+ ? AbstractMetaFunction::findClassModifications(metaFunction.get(), currentClass)
+ : AbstractMetaFunction::findGlobalModifications(metaFunction.get());
applyCachedFunctionModifications(metaFunction, functionMods);
&& metaFunction->argumentName(i + 1, false, currentClass).isEmpty()) {
qCWarning(lcShiboken, "%s",
qPrintable(msgUnnamedArgumentDefaultExpression(currentClass, i + 1,
- className, metaFunction)));
+ className, metaFunction.get())));
}
}
}
QString errorMessage;
- if (!applyArrayArgumentModifications(f->modifications(subclass), f.get(),
+ if (!applyArrayArgumentModifications(f->modifications(subclass), f,
&errorMessage)) {
qCWarning(lcShiboken, "While specializing %s (%s): %s",
qPrintable(subclass->name()), qPrintable(templateClass->name()),
const QSet<QString> &enumsDeclarations);
void traverseEnums(const ScopeModelItem &item, const AbstractMetaClassPtr &parent,
const QStringList &enumsDeclarations);
- AbstractMetaFunctionRawPtrList classFunctionList(const ScopeModelItem &scopeItem,
- AbstractMetaClass::Attributes *constructorAttributes,
- const AbstractMetaClassPtr ¤tClass);
+ AbstractMetaFunctionList classFunctionList(const ScopeModelItem &scopeItem,
+ AbstractMetaClass::Attributes *constructorAttributes,
+ const AbstractMetaClassPtr ¤tClass);
void traverseFunctions(const ScopeModelItem& item,
const AbstractMetaClassPtr &parent);
- static void applyFunctionModifications(AbstractMetaFunction *func);
+ static void applyFunctionModifications(const AbstractMetaFunctionPtr &func);
void traverseFields(const ScopeModelItem &item, const AbstractMetaClassPtr &parent);
bool traverseStreamOperator(const FunctionModelItem &functionItem,
const AbstractMetaClassPtr ¤tClass);
void traverseOperatorFunction(const FunctionModelItem &item,
const AbstractMetaClassPtr ¤tClass);
- AbstractMetaFunction *traverseAddedFunctionHelper(const AddedFunctionPtr &addedFunc,
- const AbstractMetaClassPtr &metaClass,
- QString *errorMessage);
+ AbstractMetaFunctionPtr
+ traverseAddedFunctionHelper(const AddedFunctionPtr &addedFunc,
+ const AbstractMetaClassPtr &metaClass,
+ QString *errorMessage);
bool traverseAddedGlobalFunction(const AddedFunctionPtr &addedFunc,
QString *errorMessage);
bool traverseAddedMemberFunction(const AddedFunctionPtr &addedFunc,
const AbstractMetaClassPtr ¤tClass,
AbstractMetaBuilder::RejectReason reason,
const QString &rejectReason);
- AbstractMetaFunction *traverseFunction(const FunctionModelItem &function,
- const AbstractMetaClassPtr ¤tClass);
+ AbstractMetaFunctionPtr
+ traverseFunction(const FunctionModelItem &function,
+ const AbstractMetaClassPtr ¤tClass);
std::optional<AbstractMetaField> traverseField(const VariableModelItem &field,
const AbstractMetaClassCPtr &cls);
void checkFunctionModifications() const;
* said class.
* \param metaFunction conversion operator function to be fixed.
*/
- static void fixReturnTypeOfConversionOperator(AbstractMetaFunction *metaFunction);
+ static void fixReturnTypeOfConversionOperator(const AbstractMetaFunctionPtr &metaFunction);
void parseQ_Properties(const AbstractMetaClassPtr &metaClass,
const QStringList &declarations);
void sortLists();
void setInclude(const TypeEntryPtr &te, const QString &path) const;
- static void fixArgumentNames(AbstractMetaFunction *func, const FunctionModificationList &mods);
+ static void fixArgumentNames(const AbstractMetaFunctionPtr &func,
+ const FunctionModificationList &mods);
void fillAddedFunctions(const AbstractMetaClassPtr &metaClass);
AbstractMetaClassCPtr resolveTypeSystemTypeDef(const AbstractMetaType &t) const;
m_cachedSignature += u", "_s;
m_cachedSignature += t.cppSignature();
// We need to have the argument names in the qdoc files
- m_cachedSignature += u' ';
+ if (!m_cachedSignature.endsWith(u'*') && !m_cachedSignature.endsWith(u'&'))
+ m_cachedSignature += u' ';
m_cachedSignature += a.name();
}
m_cachedSignature += u')';
using AbstractMetaFieldList = QList<AbstractMetaField>;
using AbstractMetaFunctionRawPtrList = QList<AbstractMetaFunction *>;
using AbstractMetaFunctionCList = QList<AbstractMetaFunctionCPtr>;
+using AbstractMetaFunctionList = QList<AbstractMetaFunctionPtr>;
using AbstractMetaTypeList = QList<AbstractMetaType>;
using UsingMembers = QList<UsingMember>;
#ifndef ANYSTRINGVIEW_STREAM_H
#define ANYSTRINGVIEW_STREAM_H
+#include <QtCore/QtTypes>
#include <QtCore/QtClassHelperMacros>
QT_FORWARD_DECLARE_CLASS(QAnyStringView)
const auto method = vector->findFunction("method");
QVERIFY(method);
- QCOMPARE(method->signature(), u"method(const Vector<int> & vector)");
+ QCOMPARE(method->signature(), "method(const Vector<int> &vector)"_L1);
const auto otherMethod = vector->findFunction("otherMethod");
QVERIFY(otherMethod);
{u"PyObject"_s, u"true"_s, TypeSystem::CPythonType::Other},
// shiboken-specific
{u"PyPathLike"_s, u"Shiboken::String::checkPath"_s, TypeSystem::CPythonType::Other},
- {u"PySequence"_s, u"Shiboken::String::checkIterable"_s, TypeSystem::CPythonType::Other},
+ {u"PySequence"_s, u"Shiboken::String::checkIterableArgument"_s,
+ TypeSystem::CPythonType::Other},
{u"PyUnicode"_s, u"PyUnicode_Check"_s, TypeSystem::CPythonType::String},
{u"PyTypeObject"_s, u"PyType_Check"_s, TypeSystem::CPythonType::Other},
{u"str"_s, u"Shiboken::String::check"_s, TypeSystem::CPythonType::String},
# Python_SOABI is only set by CMake 3.17+
# TODO: Lower this to CMake 3.16 if possible.
if(SHIBOKEN_IS_CROSS_BUILD)
- # For android platform armv7a FindPython module return Python_SOABI as empty because
- # it is unable to set Python_CONFIG i.e. find `python3-config` script
- # This workaround sets the Python_SOABI manually for this platform.
- if(CMAKE_SYSTEM_NAME STREQUAL "Android" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "armv7-a")
- set(Python_SOABI "cpython-311}")
- endif()
if(NOT Python_SOABI)
message(FATAL_ERROR "Python_SOABI variable is empty.")
endif()
"${_shiboken_backup_CMAKE_FIND_ROOT_PATH_MODE_PROGRAM}")
set(CMAKE_FIND_ROOT_PATH
"${_shiboken_backup_CMAKE_FIND_ROOT_PATH}")
+
+ # For Android platform sometimes the FindPython module returns Python_SOABI as empty in
+ # certain scenarios eg: armv7a target, macOS host etc. This is because
+ # it is unable to set Python_CONFIG i.e. `python3-config` script
+ # This workaround sets the Python_SOABI manually for this Android platform.
+ # This needs to be updated manually if the Python version for Android cross compilation
+ # changes.
+ # TODO: Find a better way to set Python_SOABI for Android platform
+ if(CMAKE_SYSTEM_NAME STREQUAL "Android" AND NOT Python_SOABI)
+ set(Python_SOABI "cpython-311")
+ endif()
else()
find_package(
Python
set(debug_level "")
if(SHIBOKEN_DEBUG_LEVEL)
set(debug_level "--debug-level=${SHIBOKEN_DEBUG_LEVEL}")
- elseif(DEFINED $ENV{SHIBOKEN_DEBUG_LEVEL})
+ elseif(DEFINED ENV{SHIBOKEN_DEBUG_LEVEL})
set(debug_level "--debug-level=$ENV{SHIBOKEN_DEBUG_LEVEL}")
endif()
set(${out_var} "${debug_level}" PARENT_SCOPE)
.. toctree::
:maxdepth: 1
- typesystem_arguments.rst
+ Function argument modifications <typesystem_arguments.rst>
typesystem_codeinjection.rst
typesystem_converters.rst
typesystem_containers.rst
typesystem_templates.rst
- typesystem_modify_function.rst
typesystem_manipulating_objects.rst
typesystem_conversionrule.rst
typesystem_documentation.rst
-.. _modifying-arguments:
+.. _modify-argument:
-Modifying Arguments
--------------------
+modify-argument
+---------------
-.. _conversionrule-on-arguments:
-
-conversion-rule
-^^^^^^^^^^^^^^^
-
-The ``conversion-rule`` node allows you to write customized code to convert
-the given argument between the target language and C++.
-It is then a child of the :ref:`modify-argument` node:
+Function argument modifications consist of a list of ``modify-argument`` nodes
+contained in :ref:`modify-function`, :ref:`add-function` or
+:ref:`declare-function` nodes. Nested :ref:`remove-argument`,
+:ref:`replace-default-expression`, :ref:`remove-default-expression`,
+:ref:`replace-type`, :ref:`reference-count` and :ref:`define-ownership`
+nodes specify the details of the modification.
.. code-block:: xml
- <modify-argument index="2">
- <!-- for the second argument of the function -->
- <conversion-rule class="target | native">
- // the code
- </conversion-rule>
- </modify-argument>
+ <modify-function>
+ <modify-argument index="return | this | 1 ..." rename="..."
+ invalidate-after-use = "true | false" pyi-type="...">
+ // modifications
+ </modify-argument>
+ </modify-function>
-The ``class`` attribute accepts one of the following values to define the
-conversion direction to be either ``target-to-native`` or ``native-to-target``:
+Set the ``index`` attribute to "1" for the first argument, "2" for the second
+one and so on. Alternatively, set it to "return" or "this" if you want to
+modify the function's return value or the object the function is called upon,
+respectively.
-* ``native``: Defines the conversion direction to be ``target-to-native``.
- It is similar to the existing ``<target-to-native>`` element.
- See :ref:`Conversion Rule Tag <conversion-rule-tag>` for more information.
-
-* ``target``: Defines the conversion direction to be ``native-to-target``.
- It is similar to the existing ``<native-to-target>`` element.
- See :ref:`Conversion Rule Tag <conversion-rule-tag>` for more information.
-
-This node is typically used in combination with the :ref:`replace-type` and
-:ref:`remove-argument` nodes. The given code is used instead of the generator's
-conversion code.
+The optional ``rename`` attribute is used to rename a argument and use this
+new name in the generated code. This attribute can be used to enable the usage
+of ``keyword arguments``.
-Writing %N in the code (where N is a number), will insert the name of the
-nth argument. Alternatively, %in and %out which will be replaced with the
-name of the conversion's input and output variable, respectively. Note the
-output variable must be declared explicitly, for example:
-
-.. code-block:: xml
-
- <conversion-rule class="native">
- bool %out = (bool) %in;
- </conversion-rule>
+The optional ``pyi-type`` attribute specifies the type to appear in the
+signature strings and ``.pyi`` files. The type string is determined by
+checking this attribute value, the :ref:`replace-type` modification and
+the C++ type. The attribute can be used for example to enclose
+a pointer return value within ``Optional[]`` to indicate that ``None``
+can occur.
-.. note::
+For the optional ``invalidate-after-use`` attribute,
+see :ref:`invalidationafteruse` .
- You can also use the ``conversion-rule`` node to specify
- :ref:`a conversion code which will be used instead of the generator's conversion code everywhere for a given type <conversion-rule-tag>`.
+Naming, type, default value modifications
++++++++++++++++++++++++++++++++++++++++++
.. _remove-argument:
the fully qualified name (including name of the package as well as the class
name).
+Ownership/Reference modifications
++++++++++++++++++++++++++++++++++
+
.. _define-ownership:
define-ownership
The ``define-ownership`` tag indicates that the function changes the ownership
rules of the argument object, and it is a child of the
:ref:`modify-argument` node.
+
+.. code-block:: xml
+
+ <modify-argument>
+ <define-ownership class="target | native"
+ owner="target | c++ | default" />
+ </modify-argument>
+
The ``class`` attribute specifies the class of
function where to inject the ownership altering code
(see :ref:`codegenerationterminology`). The ``owner`` attribute
specifies the new ownership of the object. It accepts the following values:
* target: the target language will assume full ownership of the object.
- The native resources will be deleted when the target language
- object is finalized.
+ The native resources will be deleted when the target language
+ object is finalized.
* c++: The native code assumes full ownership of the object. The target
- language object will not be garbage collected.
+ language object will not be garbage collected.
* default: The object will get default ownership, depending on how it
- was created.
-
-.. code-block:: xml
-
- <modify-argument>
- <define-ownership class="target | native"
- owner="target | c++ | default" />
- </modify-argument>
+ was created.
.. _reference-count:
The ``reference-count`` tag dictates how an argument should be handled by the
target language reference counting system (if there is any), it also indicates
the kind of relationship the class owning the function being modified has with
-the argument. It is a child of the :ref:`modify-argument` node.
-For instance, in a model/view relation a view receiving a model
-as argument for a **setModel** method should increment the model's reference
-counting, since the model should be kept alive as much as the view lives.
-Remember that out hypothetical view could not become parent of the model,
-since the said model could be used by other views as well.
-The ``action`` attribute specifies what should be done to the argument
-reference counting when the modified method is called. It accepts the
-following values:
-
-* add: increments the argument reference counter.
-* add-all: increments the reference counter for each item in a collection.
-* remove: decrements the argument reference counter.
-* set: will assign the argument to the variable containing the reference.
-* ignore: does nothing with the argument reference counter
- (sounds worthless, but could be used in situations
- where the reference counter increase is mandatory
- by default).
+the argument (represented as lists of referred-to objects stored in the
+owner class). It is a child of the :ref:`modify-argument` node.
.. code-block:: xml
<modify-argument>
- <reference-count action="add|add-all|remove|set|ignore" variable-name="..." />
+ <reference-count action="add|remove|set|ignore" variable-name="..." />
</modify-argument>
+The ``action`` attribute specifies what should be done to the argument
+reference counting when the modified method is called. It accepts the
+following values:
-The variable-name attribute specifies the name used for the variable that
-holds the reference(s).
-
-.. _replace-value:
-
-replace-value
-^^^^^^^^^^^^^
-
-The ``replace-value`` attribute lets you replace the return statement of a
-function with a fixed string. This attribute can only be used for the
-argument at ``index`` 0, which is always the function's return value.
+* add: Adds the argument to the list of previous argument values stored
+ under this ``variable-name`` or function signature and increments
+ the argument reference counter.
+* remove: Decrements the argument reference counter and removes it from
+ the list of argument values stored under this ``variable-name``
+ or function signature.
+* set: Decreases the reference count of the previously stored argument values
+ under this ``variable-name`` or function signature and removes them.
+ Stores the argument and increments the argument reference counter.
+* ignore: does nothing with the argument reference counter
+ (sounds worthless, but could be used in situations
+ where the reference counter increase is mandatory by default).
-.. code-block:: xml
+The ``variable-name`` attribute specifies the name used for the variable that
+holds the reference(s). It defaults to the function signature.
- <modify-argument index="0" replace-value="this"/>
+For instance, in a model/view relation, a view receiving a model
+as argument for a **setModel()** method should increment the model's reference
+counting, since the model should be kept alive as long as the view lives.
+Remember that our hypothetical view cannot become a :ref:`parent` of the
+model, since the said model could be used by other views as well.
.. _parent:
In the ``index`` argument you must specify the parent argument. The action
*add* creates a parent link between objects, while *remove* will undo the
parentage relationship.
+
+Other modifications
++++++++++++++++++++
+
+.. _conversionrule-on-arguments:
+
+conversion-rule
+^^^^^^^^^^^^^^^
+
+The ``conversion-rule`` node allows you to write customized code to convert
+the given argument between the target language and C++.
+It is then a child of the :ref:`modify-argument` node:
+
+.. code-block:: xml
+
+ <modify-argument index="2">
+ <!-- for the second argument of the function -->
+ <conversion-rule class="target | native">
+ // the code
+ </conversion-rule>
+ </modify-argument>
+
+The ``class`` attribute accepts one of the following values to define the
+conversion direction to be either ``target-to-native`` or ``native-to-target``:
+
+* ``native``: Defines the conversion direction to be ``target-to-native``.
+ It is similar to the existing ``<target-to-native>`` element.
+ See :ref:`Conversion Rule Tag <conversion-rule-tag>` for more information.
+
+* ``target``: Defines the conversion direction to be ``native-to-target``.
+ It is similar to the existing ``<native-to-target>`` element.
+ See :ref:`Conversion Rule Tag <conversion-rule-tag>` for more information.
+
+This node is typically used in combination with the :ref:`replace-type` and
+:ref:`remove-argument` nodes. The given code is used instead of the generator's
+conversion code.
+
+Writing %N in the code (where N is a number), will insert the name of the
+nth argument. Alternatively, %in and %out which will be replaced with the
+name of the conversion's input and output variable, respectively. Note the
+output variable must be declared explicitly, for example:
+
+.. code-block:: xml
+
+ <conversion-rule class="native">
+ bool %out = (bool) %in;
+ </conversion-rule>
+
+.. note::
+
+ You can also use the ``conversion-rule`` node to specify
+ :ref:`a conversion code which will be used instead of the generator's conversion code everywhere for a given type <conversion-rule-tag>`.
+
+.. _replace-value:
+
+replace-value
+^^^^^^^^^^^^^
+
+The ``replace-value`` attribute lets you replace the return statement of a
+function with a fixed string. This attribute can only be used for the
+argument at ``index`` 0, which is always the function's return value.
+
+.. code-block:: xml
+
+ <modify-argument index="0" replace-value="this"/>
The ``modify-function`` node allows you to modify a given C++ function when
mapping it onto the target language, and it is a child of a :ref:`function`,
:ref:`namespace`, :ref:`object-type` or a :ref:`value-type` node.
-Use the :ref:`modify-argument` node to specify which argument the
-modification affects.
+Nested :ref:`modify-argument` nodes can used to modify arguments
+or return values.
.. code-block:: xml
+++ /dev/null
-.. _modifying-functions:
-
-Modifying Functions
--------------------
-
-.. _modify-argument:
-
-modify-argument
-^^^^^^^^^^^^^^^
-
-Function modifications consist of a list of ``modify-argument`` nodes
-contained in :ref:`modify-function`, :ref:`add-function` or
-:ref:`declare-function` nodes. Use the :ref:`remove-argument`,
-:ref:`replace-default-expression`, :ref:`remove-default-expression`,
-:ref:`replace-type`, :ref:`reference-count` and :ref:`define-ownership`
-nodes to specify the details of the modification.
-
-.. code-block:: xml
-
- <modify-function>
- <modify-argument index="return | this | 1 ..." rename="..."
- invalidate-after-use = "true | false" pyi-type="...">
- // modifications
- </modify-argument>
- </modify-function>
-
-Set the ``index`` attribute to "1" for the first argument, "2" for the second
-one and so on. Alternatively, set it to "return" or "this" if you want to
-modify the function's return value or the object the function is called upon,
-respectively.
-
-The optional ``rename`` attribute is used to rename a argument and use this
-new name in the generated code. This attribute can be used to enable the usage
-of ``keyword arguments``.
-
-The optional ``pyi-type`` attribute specifies the type to appear in the
-signature strings and ``.pyi`` files. The type string is determined by
-checking this attribute value, the :ref:`replace-type` modification and
-the C++ type. The attribute can be used for example to enclose
-a pointer return value within ``Optional[]`` to indicate that ``None``
-can occur.
-
-For the optional ``invalidate-after-use`` attribute,
-see :ref:`invalidationafteruse` .
GeneratorContext Generator::contextForClass(const AbstractMetaClassCPtr &c) const
{
GeneratorContext result;
+ result.m_type = GeneratorContext::Class;
result.m_metaClass = c;
return result;
}
QString GeneratorContext::effectiveClassName() const
{
+ Q_ASSERT(hasClass());
if (m_type == SmartPointer)
return m_preciseClassType.cppSignature();
return m_type == WrappedClass ? m_wrappername : m_metaClass->qualifiedCppName();
friend class ShibokenGenerator;
friend class Generator;
public:
- enum Type { Class, WrappedClass, SmartPointer };
+ enum Type { Class, WrappedClass, SmartPointer,
+ GlobalFunction // No class contained
+ };
GeneratorContext() = default;
- AbstractMetaClassCPtr metaClass() const { return m_metaClass; }
- const AbstractMetaType &preciseType() const { return m_preciseClassType; }
- AbstractMetaClassCPtr pointeeClass() const { return m_pointeeClass; }
+ const AbstractMetaClassCPtr &metaClass() const
+ {
+ Q_ASSERT(hasClass());
+ return m_metaClass;
+ }
+
+ const AbstractMetaType &preciseType() const
+ {
+ Q_ASSERT(forSmartPointer());
+ return m_preciseClassType;
+ }
+
+ AbstractMetaClassCPtr pointeeClass() const
+ {
+
+ Q_ASSERT(forSmartPointer());
+ return m_pointeeClass;
+ }
bool forSmartPointer() const { return m_type == SmartPointer; }
bool useWrapper() const { return m_type == WrappedClass; }
+ bool hasClass() const { return m_type != GlobalFunction; }
QString wrapperName() const;
/// Returns the wrapper name in case of useWrapper(), the qualified class
AbstractMetaClassCPtr m_pointeeClass;
AbstractMetaType m_preciseClassType;
QString m_wrappername;
- Type m_type = Class;
+ Type m_type = GlobalFunction;
};
QDebug operator<<(QDebug debug, const GeneratorContext &c);
{
QFile inheritanceFile(m_options.inheritanceFile);
if (!inheritanceFile.open(QIODevice::WriteOnly | QIODevice::Text))
- throw Exception(msgCannotOpenForWriting(m_options.inheritanceFile));
+ throw Exception(msgCannotOpenForWriting(inheritanceFile));
QJsonObject dict;
for (const auto &c : api().classes()) {
static constexpr auto virtualMethodStaticReturnVar = "result"_L1;
static constexpr auto sbkObjectTypeF = "SbkObject_TypeF()"_L1;
+static constexpr auto enumConverterPythonType = "Enum"_L1;
static const char initInheritanceFunction[] = "initInheritance";
static QString mangleName(QString name)
c << "*reinterpret_cast<" << cppTypeName << " *>(cppOut) = value;\n";
ConfigurableScope configScope(s, enumType);
- writePythonToCppFunction(s, c.toString(), typeName, typeName);
+ writePythonToCppFunction(s, c.toString(), enumConverterPythonType, typeName);
QString pyTypeCheck = u"PyObject_TypeCheck(pyIn, "_s + enumPythonType + u')';
- writeIsPythonConvertibleToCppFunction(s, typeName, typeName, pyTypeCheck);
+ writeIsPythonConvertibleToCppFunction(s, enumConverterPythonType, typeName, pyTypeCheck);
c.clear();
c << "const int castCppIn = int(*reinterpret_cast<const "
<< cppTypeName << " *>(cppIn));\n" << "return "
<< "Shiboken::Enum::newItem(" << enumPythonType << ", castCppIn);\n";
- writeCppToPythonFunction(s, c.toString(), typeName, typeName);
+ writeCppToPythonFunction(s, c.toString(), typeName, enumConverterPythonType);
s << '\n';
}
ErrorReturn errorReturn)
{
const auto rfunc = overloadData.referenceFunction();
- const auto ownerClass = rfunc->targetLangOwner();
- Q_ASSERT(ownerClass == context.metaClass());
int minArgs = overloadData.minArgs();
int maxArgs = overloadData.maxArgs();
bool initPythonArguments;
// If method is a constructor...
if (rfunc->isConstructor()) {
+ const auto ownerClass = rfunc->targetLangOwner();
+ Q_ASSERT(ownerClass == context.metaClass());
// Check if the right constructor was called.
if (!ownerClass->hasPrivateDestructor()) {
s << "if (Shiboken::Object::isUserType(self) && "
&& !overloadData.pythonFunctionWrapperUsesListOfArguments()) {
s << "(" << PYTHON_ARG << " == 0 ? 0 : 1);\n";
} else {
- writeArgumentsInitializer(s, overloadData, errorReturn);
+ writeArgumentsInitializer(s, overloadData, context, errorReturn);
}
}
}
s << '\n';
if (overloadData.maxArgs() > 0)
- writeOverloadedFunctionDecisor(s, overloadData, errorReturn);
+ writeOverloadedFunctionDecisor(s, overloadData, classContext, errorReturn);
// Handles Python Multiple Inheritance
QString pre = needsMetaObject ? u"bool usesPyMI = "_s : u""_s;
<< "}\n";
if (overloadData.maxArgs() > 0)
s << "if (cptr == nullptr)\n" << indent
- << "return " << returnErrorWrongArguments(overloadData, errorReturn) << ";\n\n"
- << outdent;
+ << "return " << returnErrorWrongArguments(overloadData, classContext, errorReturn)
+ << ";\n\n" << outdent;
s << "Shiboken::Object::setValidCpp(sbkSelf, true);\n";
// If the created C++ object has a C++ wrapper the ownership is assigned to Python
<< "metaObject = cptr->metaObject(); // <- init python qt properties\n"
<< "if (!errInfo.isNull() && PyDict_Check(errInfo.object())) {\n" << indent
<< "if (!PySide::fillQtProperties(self, metaObject, errInfo, usesPyMI))\n" << indent
- << "return " << returnErrorWrongArguments(overloadData, errorReturn) << ";\n"
- << outdent << outdent
+ << "return " << returnErrorWrongArguments(overloadData, classContext, errorReturn)
+ << ";\n" << outdent << outdent
<< "};\n";
}
<< "// Do not enter here if other object has implemented a reverse operator.\n"
<< "if (" << PYTHON_RETURN_VAR << " == nullptr) {\n" << indent;
if (maxArgs > 0)
- writeOverloadedFunctionDecisor(s, overloadData, ErrorReturn::Default);
+ writeOverloadedFunctionDecisor(s, overloadData, classContext, ErrorReturn::Default);
writeFunctionCalls(s, overloadData, classContext, ErrorReturn::Default);
s << outdent << '\n' << "} // End of \"if (!" << PYTHON_RETURN_VAR << ")\"\n";
} else { // binary shift operator
if (maxArgs > 0)
- writeOverloadedFunctionDecisor(s, overloadData, ErrorReturn::Default);
+ writeOverloadedFunctionDecisor(s, overloadData, classContext, ErrorReturn::Default);
writeFunctionCalls(s, overloadData, classContext, ErrorReturn::Default);
}
}
void CppGenerator::writeArgumentsInitializer(TextStream &s, const OverloadData &overloadData,
+ const GeneratorContext &classContext,
ErrorReturn errorReturn)
{
const auto rfunc = overloadData.referenceFunction();
s << "errInfo.reset(Shiboken::checkInvalidArgumentCount(numArgs, "
<< minArgs << ", " << maxArgs << "));\n"
<< "if (!errInfo.isNull())\n" << indent
- << "return " << returnErrorWrongArguments(overloadData, errorReturn) << ";\n"
+ << "return " << returnErrorWrongArguments(overloadData, classContext, errorReturn) << ";\n"
<< outdent;
}
s << "numArgs == " << invalidArgsLength.at(i);
}
s << ")\n" << indent
- << "return " << returnErrorWrongArguments(overloadData, errorReturn) << ";\n"
+ << "return " << returnErrorWrongArguments(overloadData, classContext, errorReturn) << ";\n"
<< outdent;
}
s << '\n';
}
QString CppGenerator::returnErrorWrongArguments(const OverloadData &overloadData,
- ErrorReturn errorReturn)
+ const GeneratorContext &context,
+ ErrorReturn errorReturn)
{
+ Q_UNUSED(context);
const auto rfunc = overloadData.referenceFunction();
QString argsVar = overloadData.pythonFunctionWrapperUsesListOfArguments()
? u"args"_s : PYTHON_ARG;
void CppGenerator::writeOverloadedFunctionDecisor(TextStream &s,
const OverloadData &overloadData,
+ const GeneratorContext &classContext,
ErrorReturn errorReturn) const
{
s << "// Overloaded function decisor\n";
s << "// Function signature not found.\n"
<< "if (overloadId == -1)\n" << indent
- << "return " << returnErrorWrongArguments(overloadData, errorReturn) << ";\n\n"
- << outdent;
+ << "return " << returnErrorWrongArguments(overloadData, classContext, errorReturn)
+ << ";\n\n" << outdent;
}
void CppGenerator::writeOverloadedFunctionDecisorEngine(TextStream &s,
const bool usePyArgs = overloadData.pythonFunctionWrapperUsesListOfArguments();
// Handle named arguments.
- writeNamedArgumentResolution(s, func, usePyArgs, overloadData, errorReturn);
+ writeNamedArgumentResolution(s, func, usePyArgs, overloadData, context, errorReturn);
bool injectCodeCallsFunc = injectedCodeCallsCppFunction(context, func);
bool mayHaveUnunsedArguments = !func->isUserAdded() && func->hasInjectedCode() && injectCodeCallsFunc;
const AbstractMetaFunctionCPtr &func,
bool usePyArgs,
const OverloadData &overloadData,
+ const GeneratorContext &classContext,
ErrorReturn errorReturn)
{
const AbstractMetaArgumentList &args = OverloadData::getArgumentsWithDefaultValues(func);
s << "if (kwds != nullptr && PyDict_Size(kwds) > 0) {\n" << indent
<< "errInfo.reset(kwds);\n"
<< "Py_INCREF(errInfo.object());\n"
- << "return " << returnErrorWrongArguments(overloadData, errorReturn) << ";\n"
- << outdent << "}\n";
+ << "return " << returnErrorWrongArguments(overloadData, classContext, errorReturn)
+ << ";\n" << outdent << "}\n";
}
return;
}
<< "if (value != nullptr && " << pyArgName << " != nullptr ) {\n"
<< indent << "errInfo.reset(" << pyKeyName << ");\n"
<< "Py_INCREF(errInfo.object());\n"
- << "return " << returnErrorWrongArguments(overloadData, errorReturn) << ";\n"
- << outdent << "}\nif (value != nullptr) {\n" << indent
+ << "return " << returnErrorWrongArguments(overloadData, classContext, errorReturn)
+ << ";\n" << outdent << "}\nif (value != nullptr) {\n" << indent
<< pyArgName << " = value;\nif (!";
const auto &type = arg.modifiedType();
writeTypeCheck(s, type, pyArgName, isNumber(type.typeEntry()), {});
s << ")\n" << indent
- << "return " << returnErrorWrongArguments(overloadData, errorReturn) << ";\n"
- << outdent << outdent
+ << "return " << returnErrorWrongArguments(overloadData, classContext, errorReturn)
+ << ";\n" << outdent << outdent
<< "}\nPyDict_DelItem(kwds_dup, " << pyKeyName << ");\n"
<< outdent << "}\n";
}
// until extra keyword signals and properties are handled.
s << "if (PyDict_Size(kwds_dup) > 0) {\n" << indent
<< "errInfo.reset(kwds_dup.release());\n";
- if (!(func->isConstructor() && isQObject(func->ownerClass())))
- s << "return " << returnErrorWrongArguments(overloadData, errorReturn) << ";\n";
- else
+ if (!(func->isConstructor() && isQObject(func->ownerClass()))) {
+ s << "return " << returnErrorWrongArguments(overloadData, classContext, errorReturn)
+ << ";\n";
+ } else {
s << "// fall through to handle extra keyword signals and properties\n";
+ }
s << outdent << "}\n"
<< outdent << "}\n";
}
const QString typeName = fixedCppTypeName(enumType);
s << "SbkConverter *converter = Shiboken::Conversions::createConverter("
<< enumPythonVar << ',' << '\n' << indent
- << cppToPythonFunctionName(typeName, typeName) << ");\n" << outdent;
+ << cppToPythonFunctionName(typeName, enumConverterPythonType) << ");\n" << outdent;
- const QString toCpp = pythonToCppFunctionName(typeName, typeName);
- const QString isConv = convertibleToCppFunctionName(typeName, typeName);
+ QString toCpp = pythonToCppFunctionName(enumConverterPythonType, typeName);
+ const QString isConv = convertibleToCppFunctionName(enumConverterPythonType, typeName);
writeAddPythonToCppConversion(s, u"converter"_s, toCpp, isConv);
s << "Shiboken::Enum::setTypeConverter(" << enumPythonVar
<< ", converter);\n";
return converter;
}
+QString CppGenerator::typeInitStructHelper(const TypeEntryCPtr &te, const QString &varName)
+{
+ return cppApiVariableName(te->targetLangPackage()) + u'[' + varName + u']';
+}
+
+QString CppGenerator::typeInitStruct(const GeneratorContext &context)
+{
+ Q_ASSERT(context.hasClass());
+ if (context.forSmartPointer()) {
+ auto te = context.preciseType().typeEntry();
+ return typeInitStructHelper(te, getTypeIndexVariableName(context.preciseType()));
+ }
+ return typeInitStruct(context.metaClass()->typeEntry());
+}
+
QString CppGenerator::typeInitStruct(const TypeEntryCPtr &te)
{
- return cppApiVariableName(te->targetLangPackage()) + u'['
- + getTypeIndexVariableName(te) + u']';
+ return typeInitStructHelper(te, getTypeIndexVariableName(te));
}
void CppGenerator::writeExtendedConverterInitialization(TextStream &s,
const AbstractMetaFunctionCList &overloads,
const GeneratorContext &classContext) const;
static void writeArgumentsInitializer(TextStream &s, const OverloadData &overloadData,
+ const GeneratorContext &classContext,
ErrorReturn errorReturn = ErrorReturn::Default);
static void writeCppSelfConversion(TextStream &s,
const GeneratorContext &context,
ErrorReturn errorReturn);
static QString returnErrorWrongArguments(const OverloadData &overloadData,
+ const GeneratorContext &context,
ErrorReturn errorReturn);
static void writeFunctionReturnErrorCheckSection(TextStream &s,
* \param overloadData the overload data describing all the possible overloads for the function/method
*/
void writeOverloadedFunctionDecisor(TextStream &s, const OverloadData &overloadData,
+ const GeneratorContext &classContext,
ErrorReturn errorReturn) const;
/// Recursive auxiliar method to the other writeOverloadedFunctionDecisor.
void writeOverloadedFunctionDecisorEngine(TextStream &s,
const AbstractMetaFunctionCPtr &func,
bool usePyArgs,
const OverloadData &overloadData,
+ const GeneratorContext &classContext,
ErrorReturn errorReturn);
/// Returns a string containing the name of an argument for the given function and argument index.
void writeSmartPointerConverterInitialization(TextStream &s, const AbstractMetaType &ype) const;
static QString typeInitStruct(const TypeEntryCPtr &te);
+ static QString typeInitStruct(const GeneratorContext &context);
static void writeExtendedConverterInitialization(TextStream &s,
const TypeEntryCPtr &externalType,
const AbstractMetaClassCList &conversions);
void clearTpFuncs();
static QString chopType(QString s);
+ static QString typeInitStructHelper(const TypeEntryCPtr &te, const QString &varName);
+
QHash<QString, QString> m_tpFuncs;
QHash<QString, QString> m_nbFuncs;
};
return converterObject(typeEntry);
}
+static QString sbkEnumPrivate(const QString &name)
+{
+ return "PepType_SETP(reinterpret_cast<SbkEnumType *>("_L1 + name + "))"_L1;
+}
+
QString ShibokenGenerator::converterObject(const TypeEntryCPtr &type)
{
- if (isExtendedCppPrimitive(type))
- return QString::fromLatin1("Shiboken::Conversions::PrimitiveTypeConverter<%1>()")
- .arg(type->qualifiedCppName());
- if (type->isWrapperType())
- return QString::fromLatin1("PepType_SOTP(reinterpret_cast<PyTypeObject *>(%1))->converter")
- .arg(cpythonTypeNameExt(type));
+ if (isExtendedCppPrimitive(type)) {
+ return "Shiboken::Conversions::PrimitiveTypeConverter<"_L1
+ + type->qualifiedCppName() + ">()"_L1;
+ }
+
+ if (type->isWrapperType()) {
+ return "PepType_SOTP(reinterpret_cast<PyTypeObject *>("_L1
+ + cpythonTypeNameExt(type) + "))->converter"_L1;
+ }
+
if (type->isEnum() || type->isFlags())
- return QString::fromLatin1("PepType_SETP(reinterpret_cast<SbkEnumType *>(%1))->converter")
- .arg(cpythonTypeNameExt(type));
+ return sbkEnumPrivate(cpythonTypeNameExt(type)) + "->converter"_L1;
if (type->isArray()) {
- qDebug() << "Warning: no idea how to handle the Qt5 type " << type->qualifiedCppName();
+ qCWarning(lcShiboken, "Warning: no idea how to handle the Qt type \"%s\"",
+ qPrintable(type->qualifiedCppName()));
return {};
}
/* the typedef'd primitive types case */
auto pte = std::dynamic_pointer_cast<const PrimitiveTypeEntry>(type);
if (!pte) {
- qDebug() << "Warning: the Qt5 primitive type is unknown" << type->qualifiedCppName();
+ qCWarning(lcShiboken, "Warning: the Qt primitive type \"%s\" is unknown",
+ qPrintable(type->qualifiedCppName()));
return {};
}
pte = basicReferencedTypeEntry(pte);
return o == nullptr || o == Py_None;
}
-static void removeRefCountKey(SbkObject *self, const char *key)
+static void removeRefCountKey(SbkObject *self, const std::string &key)
{
if (self->d->referredObjects) {
const auto iterPair = self->d->referredObjects->equal_range(key);
}
}
-void keepReference(SbkObject *self, const char *key, PyObject *referredObject, bool append)
+void keepReference(SbkObject *self, const char *keyC, PyObject *referredObject, bool append)
{
+ std::string key(keyC);
+
if (isNone(referredObject)) {
removeRefCountKey(self, key);
return;
void removeReference(SbkObject *self, const char *key, PyObject *referredObject)
{
if (!isNone(referredObject))
- removeRefCountKey(self, key);
+ removeRefCountKey(self, std::string(key));
}
void clearReferences(SbkObject *self)
// We look into the currently active operation if we are going to call
// a method with zero arguments.
auto *frame = PyEval_GetFrame();
+ if (frame == nullptr) // PYSIDE-2796, has been observed to fail
+ return false;
#if !Py_LIMITED_API && !defined(PYPY_VERSION)
auto *f_code = PyFrame_GetCode(frame);
#else
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
#include "sbkstring.h"
+#include "sbkenum.h"
#include "sbkstaticstrings_p.h"
#include "autodecref.h"
return PyObject_HasAttr(obj, Shiboken::PyMagicName::iter());
}
+bool checkIterableArgument(PyObject *obj)
+{
+ return checkIterable(obj) && !Shiboken::Enum::check(obj);
+}
+
static PyObject *initPathLike()
{
PyObject *PathLike{};
{
LIBSHIBOKEN_API bool check(PyObject *obj);
LIBSHIBOKEN_API bool checkIterable(PyObject *obj);
+ /// Check for iterable function arguments (excluding enumerations)
+ LIBSHIBOKEN_API bool checkIterableArgument(PyObject *obj);
LIBSHIBOKEN_API bool checkPath(PyObject *path);
LIBSHIBOKEN_API bool checkType(PyTypeObject *obj);
LIBSHIBOKEN_API bool checkChar(PyObject *obj);
import sys
import types
from shibokensupport.signature import get_signature as get_sig
+from enum import Enum
"""
self.collision_track.add(thing_name)
init_signature = getattr(klass, "__signature__", None)
+ # PYSIDE-2752: Enums without values will not have a constructor, so
+ # we set the init_signature to None, to avoid having an empty pyi
+ # entry, like:
+ # class QCborTag(enum.IntEnum):
+ # or
+ # class BeginFrameFlag(enum.Flag):
+ if isinstance(klass, type(Enum)):
+ init_signature = None
# sort by class then enum value
enums.sort(key=lambda tup: (tup[1], tup[2].value))
optional_searcher = re.compile(pattern, flags=re.VERBOSE)
def optional_replacer(source):
+ # PYSIDE-2517: findChild/findChildren type hints:
+ # PlaceHolderType fix to avoid the '~' from TypeVar.__repr__
+ if "~PlaceHolderType" in str(source):
+ source = str(source).replace("~PlaceHolderType", "PlaceHolderType")
+
return optional_searcher.sub(replace, str(source))
self.optional_replacer = optional_replacer
# self.level is maintained by enum_sig.py
wr.print("import " + imp)
wr.print()
for mod, imports in filter_from_imports(FROM_IMPORTS, text):
- import_args = ', '.join(imports)
+ # Sorting, and getting uniques to avoid duplications
+ # on "Iterable" having a couple of entries.
+ import_args = ', '.join(sorted(set(imports)))
if mod is None:
# special case, a normal import
wr.print(f"import {import_args}")
else:
wr.print(f"from {mod} import {import_args}")
+ # Adding extra typing import for types that are used in
+ # the followed generated lines
+ wr.print("from typing import TypeAlias, TypeVar")
wr.print()
wr.print()
wr.print("NoneType: TypeAlias = type[None]")
+ # We use it only in QtCore at the moment, but this
+ # could be extended to other modules.
+ wr.print("PlaceHolderType = TypeVar(\"PlaceHolderType\", bound=QObject)")
wr.print()
else:
wr.print(line)
# Until we can force it to create Optional[t] again, we use this.
NoneType = type(None)
+# PYSIDE-2517: findChild/findChildren type hints:
+# Placeholder so it does not trigger an UNDEFINED error while building.
+# Later it will be bound to a QObject, within the QtCore types extensions
+PlaceHolderType = TypeVar("PlaceHolderType")
+
_S = TypeVar("_S")
MultiMap = typing.DefaultDict[str, typing.List[str]]
"int": int,
"List": ArrayLikeVariable,
"Optional": typing.Optional,
+ "Iterable": typing.Iterable,
"long": int,
"long long": int,
"nullptr": None,
# The PySide Part
def init_PySide6_QtCore():
- from PySide6.QtCore import Qt, QUrl, QDir, QKeyCombination
+ from PySide6.QtCore import Qt, QUrl, QDir, QKeyCombination, QObject
from PySide6.QtCore import QRect, QRectF, QSize, QPoint, QLocale, QByteArray
from PySide6.QtCore import QMarginsF # 5.9
from PySide6.QtCore import SignalInstance
from PySide6.QtCore import Connection
except ImportError:
pass
+
type_map.update({
"' '": " ",
"'%'": "%",
"size_t": int,
"NULL": None, # 5.6, MSVC
"nullptr": None, # 5.9
+ # PYSIDE-2517: findChild/findChildren type hints:
+ "PlaceHolderType": typing.TypeVar("PlaceHolderType", bound=QObject),
"PyBuffer": typing.Union[bytes, bytearray, memoryview],
"PyByteArray": bytearray,
"PyBytes": typing.Union[bytes, bytearray, memoryview],
# i.e. it must be an idempotent mapping.
if isinstance(thing, str):
return thing
+ # PYSIDE-2517: findChild/findChildren type hints:
+ # TypeVar doesn't have a __qualname__ attribute,
+ # so we fall back to use __name__ before the next condition.
+ if isinstance(thing, typing.TypeVar):
+ return get_name(thing)
if hasattr(thing, "__name__") and thing.__module__ != "typing":
m = thing.__module__
dot = "." in str(thing) or m not in (thing.__qualname__, "builtins")
task_number_match = task_number_re.match(task)
if task_number_match:
task_number = int(task_number_match.group(1))
- entry = {"title": title, "task": task, "task-number": task_number}
+ entry = {"title": title, "task": task, "task-number": str(task_number)}
if "shiboken" in title:
if sha not in shiboken6_commits:
shiboken6_commits[sha] = entry
def sort_dict(d: Dict[str, Dict[str, str]]) -> Dict[str, Dict[str, str]]:
- return dict(sorted(d.items(), key=lambda kv: kv[1]['task-number']))
+ return dict(sorted(d.items(), key=lambda kv: int(kv[1]['task-number'])))
def sort_changelog(c: List[Tuple[int, str]]) -> List[Tuple[int, str]]:
import logging
import shutil
+import re
import os
import stat
import sys
# the tag number does not matter much since we update the sdk later
DEFAULT_SDK_TAG = 6514223
ANDROID_NDK_VERSION = "26b"
+ANDROID_NDK_VERSION_NUMBER_SUFFIX = "10909125"
def run_command(command: List[str], cwd: str = None, ignore_fail: bool = False,
accept_prompts=accept_license, show_stdout=show_stdout)
-def _unpack(zip_file: Path, destination: Path):
+def extract_zip(file: Path, destination: Path):
"""
- Unpacks the zip_file into destination preserving all permissions
+ Unpacks the zip file into destination preserving all permissions
TODO: Try to use zipfile module. Currently we cannot use zipfile module here because
extractAll() does not preserve permissions.
raise RuntimeError("Unable to find program unzip. Use `sudo apt-get install unzip`"
"to install it")
- command = [unzip, zip_file, "-d", destination]
+ command = [unzip, str(file), "-d", str(destination)]
run_command(command=command, show_stdout=True)
+def extract_dmg(file: Path, destination: Path):
+ output = run_command(['hdiutil', 'attach', '-nobrowse', '-readonly', str(file)],
+ show_stdout=True, capture_stdout=True)
+
+ # find the mounted volume
+ result = re.search(r'/Volumes/(.*)', output)
+ if not result:
+ raise RuntimeError(f"Unable to find mounted volume for file {file}")
+ mounted_vol_name = result.group(1)
+ if not mounted_vol_name:
+ raise RuntimeError(f"Unable to find mounted volume for file {file}")
+
+ # copy files
+ shutil.copytree(f'/Volumes/{mounted_vol_name}/', destination, dirs_exist_ok=True)
+
+ # Detach mounted volume
+ run_command(['hdiutil', 'detach', f'/Volumes/{mounted_vol_name}'])
+
+
def _download(url: str, destination: Path):
"""
Download url to destination
with DownloadProgressBar(unit='B', unit_scale=True, miniters=1, desc=url.split('/')[-1]) as t:
download_path, headers = request.urlretrieve(url=url, filename=destination,
reporthook=t.update_to)
- assert headers["Content-Type"] == "application/zip"
assert Path(download_path).resolve() == destination
Downloads the given ndk_version into ndk_path
"""
ndk_path = ndk_path / "android-ndk"
- ndk_zip_path = ndk_path / f"android-ndk-r{ANDROID_NDK_VERSION}-linux.zip"
- ndk_version_path = ndk_path / f"android-ndk-r{ANDROID_NDK_VERSION}"
+ ndk_extension = "dmg" if sys.platform == "darwin" else "zip"
+ ndk_zip_path = ndk_path / f"android-ndk-r{ANDROID_NDK_VERSION}-{sys.platform}.{ndk_extension}"
+ ndk_version_path = ""
+ if sys.platform == "linux":
+ ndk_version_path = ndk_path / f"android-ndk-r{ANDROID_NDK_VERSION}"
+ elif sys.platform == "darwin":
+ ndk_version_path = (ndk_path
+ / f"AndroidNDK{ANDROID_NDK_VERSION_NUMBER_SUFFIX}.app/Contents/NDK")
+ else:
+ raise RuntimeError(f"Unsupported platform {sys.platform}")
if ndk_version_path.exists():
print(f"NDK path found in {str(ndk_version_path)}")
else:
ndk_path.mkdir(parents=True, exist_ok=True)
url = (f"https://dl.google.com/android/repository"
- f"/android-ndk-r{ANDROID_NDK_VERSION}-linux.zip")
+ f"/android-ndk-r{ANDROID_NDK_VERSION}-{sys.platform}.{ndk_extension}")
print(f"Downloading Android Ndk version r{ANDROID_NDK_VERSION}")
_download(url=url, destination=ndk_zip_path)
print("Unpacking Android Ndk")
- _unpack(zip_file=(ndk_path / f"android-ndk-r{ANDROID_NDK_VERSION}-linux.zip"),
- destination=ndk_path)
+ if sys.platform == "darwin":
+ extract_dmg(file=(ndk_path
+ / f"android-ndk-r{ANDROID_NDK_VERSION}-{sys.platform}.{ndk_extension}"
+ ),
+ destination=ndk_path)
+ ndk_version_path = (ndk_version_path
+ / f"AndroidNDK{ANDROID_NDK_VERSION_NUMBER_SUFFIX}.app/Contents/NDK")
+ else:
+ extract_zip(file=(ndk_path
+ / f"android-ndk-r{ANDROID_NDK_VERSION}-{sys.platform}.{ndk_extension}"
+ ),
+ destination=ndk_path)
return ndk_version_path
"""
Downloads Android commandline tools into cltools_path.
"""
+ sdk_platform = sys.platform if sys.platform != "darwin" else "mac"
android_sdk_dir = android_sdk_dir / "android-sdk"
url = ("https://dl.google.com/android/repository/"
- f"commandlinetools-linux-{DEFAULT_SDK_TAG}_latest.zip")
- cltools_zip_path = android_sdk_dir / f"commandlinetools-linux-{DEFAULT_SDK_TAG}_latest.zip"
+ f"commandlinetools-{sdk_platform}-{DEFAULT_SDK_TAG}_latest.zip")
+ cltools_zip_path = (android_sdk_dir
+ / f"commandlinetools-{sdk_platform}-{DEFAULT_SDK_TAG}_latest.zip")
cltools_path = android_sdk_dir / "tools"
if cltools_path.exists():
android_sdk_dir.mkdir(parents=True, exist_ok=True)
print("Download Android Command Line Tools: "
- f"commandlinetools-linux-{DEFAULT_SDK_TAG}_latest.zip")
+ f"commandlinetools-{sys.platform}-{DEFAULT_SDK_TAG}_latest.zip")
_download(url=url, destination=cltools_zip_path)
print("Unpacking Android Command Line Tools")
- _unpack(zip_file=cltools_zip_path, destination=android_sdk_dir)
+ extract_zip(file=cltools_zip_path, destination=android_sdk_dir)
return android_sdk_dir
if not available_build_tools_v:
raise RuntimeError('Unable to find any build tools available for download')
+ # find the latest build tools version that is not a release candidate
+ # release candidates end has rc in the version number
+ available_build_tools_v = [v for v in available_build_tools_v if "rc" not in str(v)]
+
return max(available_build_tools_v)
download_android_ndk, install_android_packages)
# Note: Does not work with PyEnv. Your Host Python should contain openssl.
+# also update the version in ShibokenHelpers.cmake if Python version changes.
PYTHON_VERSION = "3.11"
SKIP_UPDATE_HELP = ("skip the updation of SDK packages build-tools, platform-tools to"
parser.add_argument("-v", "--verbose", help="run in verbose mode", action="store_const",
dest="loglevel", const=logging.INFO)
- parser.add_argument("--api-level", type=str, default="33", help="Android API level to use")
+ parser.add_argument("--api-level", type=str, default="26",
+ help="Minimum Android API level to use")
parser.add_argument("--ndk-path", type=str, help="Path to Android NDK (Preferred r25c)")
# sdk path is needed to compile all the Qt Java Acitivity files into Qt6AndroidBindings.jar
parser.add_argument("--sdk-path", type=str, help="Path to Android SDK")
platform_data = PlatformData("x86_64", api_level, "x86_64", "x86_64", "x86-64", "64")
# python path is valid, if Python for android installation exists in python_path
- python_path = (pyside6_deploy_cache / f"Python-{platform_data.plat_name}-linux-android"
- / "_install")
+ python_path = (pyside6_deploy_cache
+ / f"Python-{platform_data.plat_name}-linux-android" / "_install")
valid_python_path = python_path.exists()
if Path(python_path).exists():
expected_dirs = ["lib", "include"]
)
if not python_ccompile_script.exists():
+ host_system_config_name = run_command("./config.guess", cwd=cpython_dir,
+ dry_run=dry_run, show_stdout=True,
+ capture_stdout=True).strip()
+
# use jinja2 to create cross_compile.sh script
template = environment.get_template("cross_compile.tmpl.sh")
content = template.render(
ndk_path=ndk_path,
api_level=platform_data.api_level,
android_py_install_path_prefix=pyside6_deploy_cache,
- host_python_path=sys.executable
+ host_python_path=sys.executable,
+ python_version=PYTHON_VERSION,
+ host_system_name=host_system_config_name,
+ host_platform_name=sys.platform
)
logging.info(f"Writing Python cross compile script into {python_ccompile_script}")
run_command([f"./{python_ccompile_script.name}"], cwd=cpython_dir, dry_run=dry_run,
show_stdout=True)
- # run patchelf to change the SONAME of libpython from libpython3.x.so.1.0 to
- # libpython3.x.so, to match with python_for_android's Python library. Otherwise,
- # the Qfp binaries won't be able to link to Python
- run_command(["patchelf", "--set-soname", f"libpython{PYTHON_VERSION}.so",
- f"libpython{PYTHON_VERSION}.so.1.0"], cwd=Path(python_path) / "lib",
- dry_run=dry_run)
-
logging.info(
f"Cross compile Python for Android platform {platform_data.plat_name}. "
f"Final installation in {python_path}"
# give run permission to cross compile script
qfp_toolchain.chmod(qfp_toolchain.stat().st_mode | stat.S_IEXEC)
+ if sys.platform == "linux":
+ host_qt_install_suffix = "gcc_64"
+ elif sys.platform == "darwin":
+ host_qt_install_suffix = "macos"
+ else:
+ raise RuntimeError("Qt for Python cross compilation not supported on this platform")
+
# run the cross compile script
logging.info(f"Running Qt for Python cross-compile for platform {platform_data.plat_name}")
qfp_ccompile_cmd = [sys.executable, "setup.py", "bdist_wheel", "--parallel=9",
"--standalone",
f"--cmake-toolchain-file={str(qfp_toolchain.resolve())}",
- f"--qt-host-path={qt_install_path}/gcc_64",
+ f"--qt-host-path={qt_install_path}/{host_qt_install_suffix}",
f"--plat-name=android_{platform_data.plat_name}",
f"--python-target-path={python_path}",
(f"--qt-target-path={qt_install_path}/"
- f"android_{platform_data.qt_plat_name}"),
+ f"android_{platform_data.qt_plat_name}"),
"--no-qt-tools"]
run_command(qfp_ccompile_cmd, cwd=pyside_setup_dir, dry_run=dry_run, show_stdout=True)
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
set -x -e
export HOST_ARCH={{ plat_name }}-linux-android
-export TOOLCHAIN={{ ndk_path }}/toolchains/llvm/prebuilt/linux-x86_64/bin
+export TOOLCHAIN={{ ndk_path }}/toolchains/llvm/prebuilt/{{ host_platform_name }}-x86_64/bin
export TOOL_PREFIX=$TOOLCHAIN/$HOST_ARCH
export PLATFORM_API={{ api_level }}
{% if plat_name == "armv7a" -%}
export LD=$TOOLCHAIN/ld
export READELF=$TOOLCHAIN/llvm-readelf
export CFLAGS='-fPIC -DANDROID'
-./configure --host=$HOST_ARCH --target=$HOST_ARCH --build=x86_64-pc-linux-gnu \
+./configure --host=$HOST_ARCH --target=$HOST_ARCH --build={{ host_system_name }} \
--with-build-python={{ host_python_path }} --enable-shared \
--enable-ipv6 ac_cv_file__dev_ptmx=yes ac_cv_file__dev_ptc=no --without-ensurepip \
ac_cv_little_endian_double=yes
-make BLDSHARED="$CC -shared" CROSS-COMPILE=$TOOL_PREFIX- CROSS_COMPILE_TARGET=yes
-make install BLDSHARED="$CC -shared" CROSS-COMPILE=$TOOL_PREFIX- \
-CROSS_COMPILE_TARGET=yes prefix={{ android_py_install_path_prefix }}/Python-$HOST_ARCH/_install
+make BLDSHARED="$CC -shared" CROSS-COMPILE=$TOOL_PREFIX- CROSS_COMPILE_TARGET=yes \
+INSTSONAME=libpython{{ python_version }}.so
+make install prefix={{ android_py_install_path_prefix }}/Python-$HOST_ARCH/_install
return "\n".join(new_s)
-def make_zip_archive(zip_name, src, skip_dirs=None):
+def make_zip_archive(zip_file, src, skip_dirs=None):
src_path = Path(src).expanduser().resolve(strict=True)
if skip_dirs is None:
skip_dirs = []
if not isinstance(skip_dirs, list):
print("Error: A list needs to be passed for 'skip_dirs'")
return
- with zipfile.ZipFile(src_path.parents[0] / Path(zip_name), 'w', zipfile.ZIP_DEFLATED) as zf:
+ with zipfile.ZipFile(zip_file, 'w', zipfile.ZIP_DEFLATED) as zf:
for file in src_path.rglob('*'):
skip = False
_parts = file.relative_to(src_path).parts
content = "\n"
# Prepare ZIP file, and copy to final destination
- zip_name = f"{project_dir.name}.zip"
- make_zip_archive(zip_name, project_dir, skip_dirs=["doc"])
- zip_src = f"{project_dir}.zip"
- zip_dst = EXAMPLES_DOC / zip_name
- shutil.move(zip_src, zip_dst)
+ # Handle examples which only have a dummy pyproject file in the "doc" dir
+ zip_root = project_dir.parent if project_dir.name == "doc" else project_dir
+ zip_name = f"{zip_root.name}.zip"
+ make_zip_archive(EXAMPLES_DOC / zip_name, zip_root, skip_dirs=["doc"])
if file_format == Format.RST:
content += f":download:`Download this example <{zip_name}>`\n\n"
--- /dev/null
+#!/bin/bash
+# Copyright (C) 2024 The Qt Company Ltd.
+# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+
+if [ ! -d "/Users/qt/python311/bin" ]; then
+ cd /Users/qt/work
+ curl -O https://www.python.org/ftp/python/3.11.9/Python-3.11.9.tar.xz
+ if [ $? -ne 0 ]; then
+ echo "Failed to download Python source code."
+ exit 1
+ fi
+
+ tar xJf Python-3.11.9.tar.xz
+ if [ $? -ne 0 ]; then
+ echo "Failed to extract Python source code."
+ exit 1
+ fi
+
+ cd Python-3.11.9/
+ ./configure --prefix=/Users/qt/python311 --with-openssl=/usr/local/opt/openssl --enable-optimizations
+ if [ $? -ne 0 ]; then
+ echo "Failed to configure Python."
+ exit 1
+ fi
+ make
+ if [ $? -ne 0 ]; then
+ echo "Failed to compile Python."
+ exit 1
+ fi
+ make install
+ if [ $? -ne 0 ]; then
+ echo "Failed to install Python."
+ exit 1
+ fi
+fi
return result
-def get_snippet_override(start_id: str, rel_path: str) -> List[str]:
+def get_snippet_override(start_id: str, rel_path: Path) -> List[str]:
"""Check if the snippet is overridden by a local file under
sources/pyside6/doc/snippets."""
file_start_id = start_id.replace(' ', '_')
indicated by pattern ("//! [1]") and return them as a dict by <id>."""
snippets: Dict[str, List[str]] = {}
snippet: List[str]
- done_snippets : List[str] = []
+ done_snippets: List[str] = []
i = 0
while i < len(lines):
# Find the end of the snippet
j = i
while j < len(lines):
- l = lines[j]
+ line = lines[j]
j += 1
# Add the line to the snippet
- snippet.append(l)
+ snippet.append(line)
# Check if the snippet is complete
- if start_id in get_snippet_ids(l, pattern):
+ if start_id in get_snippet_ids(line, pattern):
# End of snippet
snippet[len(snippet) - 1] = id_line
snippets[start_id] = snippet
return snippets
-def get_python_example_snippet_override(start_id: str, rel_path: str) -> List[str]:
+def get_python_example_snippet_override(start_id: str, rel_path: Path) -> List[str]:
"""Check if the snippet is overridden by a python example snippet."""
key = (os.fspath(rel_path), start_id)
value = python_example_snippet_mapping().get(key)
return overriden_snippet_lines(lines, start_id)
-def get_snippets(lines: List[str], rel_path: str) -> List[List[str]]:
+def get_snippets(lines: List[str], rel_path: Path) -> List[List[str]]:
"""Extract (potentially overlapping) snippets from a C++ file indicated
by '//! [1]'."""
result = _get_snippets(lines, '//', CPP_SNIPPET_PATTERN)
if snippet:
result[snippet_id] = snippet
- return result.values()
+ return list(result.values())
def get_license_from_file(lines):
failed = 0
count = len(options.files)
for i, file in enumerate(options.files):
- print(f'{i+1}/{count} {file}')
+ print(f'{i + 1}/{count} {file}')
if not test_file(file, options.uic):
failed += 1
if failed != 0: